vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

148
vendor/k8s.io/kubernetes/pkg/kubelet/dockershim/BUILD generated vendored Normal file
View File

@ -0,0 +1,148 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"convert.go",
"doc.go",
"docker_checkpoint.go",
"docker_container.go",
"docker_image.go",
"docker_image_unsupported.go",
"docker_sandbox.go",
"docker_service.go",
"docker_stats_unsupported.go",
"docker_streaming.go",
"exec.go",
"helpers.go",
"helpers_unsupported.go",
"naming.go",
"security_context.go",
"selinux_util.go",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"docker_image_linux.go",
"docker_stats_linux.go",
"helpers_linux.go",
],
"@io_bazel_rules_go//go/platform:windows_amd64": [
"docker_image_windows.go",
"docker_stats_windows.go",
"helpers_windows.go",
],
"//conditions:default": [],
}),
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim",
deps = [
"//pkg/credentialprovider:go_default_library",
"//pkg/kubelet/apis/cri:go_default_library",
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
"//pkg/kubelet/apis/kubeletconfig:go_default_library",
"//pkg/kubelet/cm:go_default_library",
"//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/dockershim/cm:go_default_library",
"//pkg/kubelet/dockershim/libdocker:go_default_library",
"//pkg/kubelet/dockershim/metrics:go_default_library",
"//pkg/kubelet/leaky:go_default_library",
"//pkg/kubelet/network:go_default_library",
"//pkg/kubelet/network/cni:go_default_library",
"//pkg/kubelet/network/hostport:go_default_library",
"//pkg/kubelet/network/kubenet:go_default_library",
"//pkg/kubelet/qos:go_default_library",
"//pkg/kubelet/server/streaming:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/kubelet/util/cache:go_default_library",
"//pkg/kubelet/util/ioutils:go_default_library",
"//pkg/kubelet/util/store:go_default_library",
"//pkg/security/apparmor:go_default_library",
"//pkg/util/filesystem:go_default_library",
"//pkg/util/hash:go_default_library",
"//pkg/util/parsers:go_default_library",
"//vendor/github.com/armon/circbuf:go_default_library",
"//vendor/github.com/blang/semver:go_default_library",
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/docker/docker/api/types/container:go_default_library",
"//vendor/github.com/docker/docker/api/types/filters:go_default_library",
"//vendor/github.com/docker/docker/api/types/strslice:go_default_library",
"//vendor/github.com/docker/docker/pkg/jsonmessage:go_default_library",
"//vendor/github.com/docker/go-connections/nat:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"convert_test.go",
"docker_checkpoint_test.go",
"docker_container_test.go",
"docker_image_test.go",
"docker_sandbox_test.go",
"docker_service_test.go",
"helpers_test.go",
"naming_test.go",
"security_context_test.go",
"selinux_util_test.go",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"helpers_linux_test.go",
],
"//conditions:default": [],
}),
data = [
],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim",
library = ":go_default_library",
deps = [
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
"//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/container/testing:go_default_library",
"//pkg/kubelet/dockershim/libdocker:go_default_library",
"//pkg/kubelet/dockershim/testing:go_default_library",
"//pkg/kubelet/network:go_default_library",
"//pkg/kubelet/network/testing:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/kubelet/util/cache:go_default_library",
"//pkg/security/apparmor:go_default_library",
"//vendor/github.com/blang/semver:go_default_library",
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/docker/docker/api/types/container:go_default_library",
"//vendor/github.com/docker/docker/pkg/jsonmessage:go_default_library",
"//vendor/github.com/docker/go-connections/nat:go_default_library",
"//vendor/github.com/golang/mock/gomock:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/kubelet/dockershim/cm:all-srcs",
"//pkg/kubelet/dockershim/libdocker:all-srcs",
"//pkg/kubelet/dockershim/metrics:all-srcs",
"//pkg/kubelet/dockershim/remote:all-srcs",
"//pkg/kubelet/dockershim/testing:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"container_manager.go",
"container_manager_unsupported.go",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"container_manager_linux.go",
],
"@io_bazel_rules_go//go/platform:windows_amd64": [
"container_manager_windows.go",
],
"//conditions:default": [],
}),
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/cm",
deps = [
"//pkg/kubelet/dockershim/libdocker:go_default_library",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"//pkg/kubelet/cm:go_default_library",
"//pkg/kubelet/qos:go_default_library",
"//pkg/util/version:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs:go_default_library",
"//vendor/github.com/opencontainers/runc/libcontainer/configs:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
],
"//conditions:default": [],
}),
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,21 @@
/*
Copyright 2016 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 cm
type ContainerManager interface {
Start() error
}

View File

@ -0,0 +1,148 @@
// +build linux
/*
Copyright 2016 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 cm
import (
"fmt"
"io/ioutil"
"regexp"
"strconv"
"time"
"github.com/golang/glog"
"github.com/opencontainers/runc/libcontainer/cgroups/fs"
"github.com/opencontainers/runc/libcontainer/configs"
"k8s.io/apimachinery/pkg/util/wait"
kubecm "k8s.io/kubernetes/pkg/kubelet/cm"
"k8s.io/kubernetes/pkg/kubelet/qos"
utilversion "k8s.io/kubernetes/pkg/util/version"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
const (
// The percent of the machine memory capacity.
dockerMemoryLimitThresholdPercent = kubecm.DockerMemoryLimitThresholdPercent
// The minimum memory limit allocated to docker container.
minDockerMemoryLimit = kubecm.MinDockerMemoryLimit
// The Docker OOM score adjustment.
dockerOOMScoreAdj = qos.DockerOOMScoreAdj
)
var (
memoryCapacityRegexp = regexp.MustCompile(`MemTotal:\s*([0-9]+) kB`)
)
func NewContainerManager(cgroupsName string, client libdocker.Interface) ContainerManager {
return &containerManager{
cgroupsName: cgroupsName,
client: client,
}
}
type containerManager struct {
// Docker client.
client libdocker.Interface
// Name of the cgroups.
cgroupsName string
// Manager for the cgroups.
cgroupsManager *fs.Manager
}
func (m *containerManager) Start() error {
// TODO: check if the required cgroups are mounted.
if len(m.cgroupsName) != 0 {
manager, err := createCgroupManager(m.cgroupsName)
if err != nil {
return err
}
m.cgroupsManager = manager
}
go wait.Until(m.doWork, 5*time.Minute, wait.NeverStop)
return nil
}
func (m *containerManager) doWork() {
v, err := m.client.Version()
if err != nil {
glog.Errorf("Unable to get docker version: %v", err)
return
}
version, err := utilversion.ParseGeneric(v.APIVersion)
if err != nil {
glog.Errorf("Unable to parse docker version %q: %v", v.APIVersion, err)
return
}
// EnsureDockerInContainer does two things.
// 1. Ensure processes run in the cgroups if m.cgroupsManager is not nil.
// 2. Ensure processes have the OOM score applied.
if err := kubecm.EnsureDockerInContainer(version, dockerOOMScoreAdj, m.cgroupsManager); err != nil {
glog.Errorf("Unable to ensure the docker processes run in the desired containers")
}
}
func createCgroupManager(name string) (*fs.Manager, error) {
var memoryLimit uint64
memoryCapacity, err := getMemoryCapacity()
if err != nil || memoryCapacity*dockerMemoryLimitThresholdPercent/100 < minDockerMemoryLimit {
memoryLimit = minDockerMemoryLimit
}
glog.V(2).Infof("Configure resource-only container %q with memory limit: %d", name, memoryLimit)
allowAllDevices := true
cm := &fs.Manager{
Cgroups: &configs.Cgroup{
Parent: "/",
Name: name,
Resources: &configs.Resources{
Memory: int64(memoryLimit),
MemorySwap: -1,
AllowAllDevices: &allowAllDevices,
},
},
}
return cm, nil
}
// getMemoryCapacity returns the memory capacity on the machine in bytes.
func getMemoryCapacity() (uint64, error) {
out, err := ioutil.ReadFile("/proc/meminfo")
if err != nil {
return 0, err
}
return parseCapacity(out, memoryCapacityRegexp)
}
// parseCapacity matches a Regexp in a []byte, returning the resulting value in bytes.
// Assumes that the value matched by the Regexp is in KB.
func parseCapacity(b []byte, r *regexp.Regexp) (uint64, error) {
matches := r.FindSubmatch(b)
if len(matches) != 2 {
return 0, fmt.Errorf("failed to match regexp in output: %q", string(b))
}
m, err := strconv.ParseUint(string(matches[1]), 10, 64)
if err != nil {
return 0, err
}
// Convert to bytes.
return m * 1024, err
}

View File

@ -0,0 +1,36 @@
// +build !linux,!windows
/*
Copyright 2016 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 cm
import (
"fmt"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
type unsupportedContainerManager struct {
}
func NewContainerManager(_ string, _ libdocker.Interface) ContainerManager {
return &unsupportedContainerManager{}
}
func (m *unsupportedContainerManager) Start() error {
return fmt.Errorf("Container Manager is unsupported in this build")
}

View File

@ -0,0 +1,35 @@
// +build windows
/*
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 cm
import (
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
// no-op
type containerManager struct {
}
func NewContainerManager(_ string, _ libdocker.Interface) ContainerManager {
return &containerManager{}
}
func (m *containerManager) Start() error {
return nil
}

View File

@ -0,0 +1,177 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"strings"
"time"
dockertypes "github.com/docker/docker/api/types"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
// This file contains helper functions to convert docker API types to runtime
// API types, or vice versa.
func imageToRuntimeAPIImage(image *dockertypes.ImageSummary) (*runtimeapi.Image, error) {
if image == nil {
return nil, fmt.Errorf("unable to convert a nil pointer to a runtime API image")
}
size := uint64(image.VirtualSize)
return &runtimeapi.Image{
Id: image.ID,
RepoTags: image.RepoTags,
RepoDigests: image.RepoDigests,
Size_: size,
}, nil
}
func imageInspectToRuntimeAPIImage(image *dockertypes.ImageInspect) (*runtimeapi.Image, error) {
if image == nil || image.Config == nil {
return nil, fmt.Errorf("unable to convert a nil pointer to a runtime API image")
}
size := uint64(image.VirtualSize)
runtimeImage := &runtimeapi.Image{
Id: image.ID,
RepoTags: image.RepoTags,
RepoDigests: image.RepoDigests,
Size_: size,
}
uid, username := getUserFromImageUser(image.Config.User)
if uid != nil {
runtimeImage.Uid = &runtimeapi.Int64Value{Value: *uid}
}
runtimeImage.Username = username
return runtimeImage, nil
}
func toPullableImageID(id string, image *dockertypes.ImageInspect) string {
// Default to the image ID, but if RepoDigests is not empty, use
// the first digest instead.
imageID := DockerImageIDPrefix + id
if len(image.RepoDigests) > 0 {
imageID = DockerPullableImageIDPrefix + image.RepoDigests[0]
}
return imageID
}
func toRuntimeAPIContainer(c *dockertypes.Container) (*runtimeapi.Container, error) {
state := toRuntimeAPIContainerState(c.Status)
if len(c.Names) == 0 {
return nil, fmt.Errorf("unexpected empty container name: %+v", c)
}
metadata, err := parseContainerName(c.Names[0])
if err != nil {
return nil, err
}
labels, annotations := extractLabels(c.Labels)
sandboxID := c.Labels[sandboxIDLabelKey]
// The timestamp in dockertypes.Container is in seconds.
createdAt := c.Created * int64(time.Second)
return &runtimeapi.Container{
Id: c.ID,
PodSandboxId: sandboxID,
Metadata: metadata,
Image: &runtimeapi.ImageSpec{Image: c.Image},
ImageRef: c.ImageID,
State: state,
CreatedAt: createdAt,
Labels: labels,
Annotations: annotations,
}, nil
}
func toDockerContainerStatus(state runtimeapi.ContainerState) string {
switch state {
case runtimeapi.ContainerState_CONTAINER_CREATED:
return "created"
case runtimeapi.ContainerState_CONTAINER_RUNNING:
return "running"
case runtimeapi.ContainerState_CONTAINER_EXITED:
return "exited"
case runtimeapi.ContainerState_CONTAINER_UNKNOWN:
fallthrough
default:
return "unknown"
}
}
func toRuntimeAPIContainerState(state string) runtimeapi.ContainerState {
// Parse the state string in dockertypes.Container. This could break when
// we upgrade docker.
switch {
case strings.HasPrefix(state, libdocker.StatusRunningPrefix):
return runtimeapi.ContainerState_CONTAINER_RUNNING
case strings.HasPrefix(state, libdocker.StatusExitedPrefix):
return runtimeapi.ContainerState_CONTAINER_EXITED
case strings.HasPrefix(state, libdocker.StatusCreatedPrefix):
return runtimeapi.ContainerState_CONTAINER_CREATED
default:
return runtimeapi.ContainerState_CONTAINER_UNKNOWN
}
}
func toRuntimeAPISandboxState(state string) runtimeapi.PodSandboxState {
// Parse the state string in dockertypes.Container. This could break when
// we upgrade docker.
switch {
case strings.HasPrefix(state, libdocker.StatusRunningPrefix):
return runtimeapi.PodSandboxState_SANDBOX_READY
default:
return runtimeapi.PodSandboxState_SANDBOX_NOTREADY
}
}
func containerToRuntimeAPISandbox(c *dockertypes.Container) (*runtimeapi.PodSandbox, error) {
state := toRuntimeAPISandboxState(c.Status)
if len(c.Names) == 0 {
return nil, fmt.Errorf("unexpected empty sandbox name: %+v", c)
}
metadata, err := parseSandboxName(c.Names[0])
if err != nil {
return nil, err
}
labels, annotations := extractLabels(c.Labels)
// The timestamp in dockertypes.Container is in seconds.
createdAt := c.Created * int64(time.Second)
return &runtimeapi.PodSandbox{
Id: c.ID,
Metadata: metadata,
State: state,
CreatedAt: createdAt,
Labels: labels,
Annotations: annotations,
}, nil
}
func checkpointToRuntimeAPISandbox(id string, checkpoint *PodSandboxCheckpoint) *runtimeapi.PodSandbox {
state := runtimeapi.PodSandboxState_SANDBOX_NOTREADY
return &runtimeapi.PodSandbox{
Id: id,
Metadata: &runtimeapi.PodSandboxMetadata{
Name: checkpoint.Name,
Namespace: checkpoint.Namespace,
},
State: state,
}
}

View File

@ -0,0 +1,71 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"testing"
dockertypes "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func TestConvertDockerStatusToRuntimeAPIState(t *testing.T) {
testCases := []struct {
input string
expected runtimeapi.ContainerState
}{
{input: "Up 5 hours", expected: runtimeapi.ContainerState_CONTAINER_RUNNING},
{input: "Exited (0) 2 hours ago", expected: runtimeapi.ContainerState_CONTAINER_EXITED},
{input: "Created", expected: runtimeapi.ContainerState_CONTAINER_CREATED},
{input: "Random string", expected: runtimeapi.ContainerState_CONTAINER_UNKNOWN},
}
for _, test := range testCases {
actual := toRuntimeAPIContainerState(test.input)
assert.Equal(t, test.expected, actual)
}
}
func TestConvertToPullableImageID(t *testing.T) {
testCases := []struct {
id string
image *dockertypes.ImageInspect
expected string
}{
{
id: "image-1",
image: &dockertypes.ImageInspect{
RepoDigests: []string{"digest-1"},
},
expected: DockerPullableImageIDPrefix + "digest-1",
},
{
id: "image-2",
image: &dockertypes.ImageInspect{
RepoDigests: []string{},
},
expected: DockerImageIDPrefix + "image-2",
},
}
for _, test := range testCases {
actual := toPullableImageID(test.id, test.image)
assert.Equal(t, test.expected, actual)
}
}

18
vendor/k8s.io/kubernetes/pkg/kubelet/dockershim/doc.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
/*
Copyright 2016 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.
*/
// Docker integration using pkg/kubelet/apis/cri/v1alpha1/runtime/api.pb.go
package dockershim

View File

@ -0,0 +1,153 @@
/*
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 dockershim
import (
"encoding/json"
"fmt"
"hash/fnv"
"path/filepath"
"github.com/golang/glog"
utilstore "k8s.io/kubernetes/pkg/kubelet/util/store"
utilfs "k8s.io/kubernetes/pkg/util/filesystem"
hashutil "k8s.io/kubernetes/pkg/util/hash"
)
const (
// default directory to store pod sandbox checkpoint files
sandboxCheckpointDir = "sandbox"
protocolTCP = Protocol("tcp")
protocolUDP = Protocol("udp")
schemaVersion = "v1"
)
type Protocol string
// PortMapping is the port mapping configurations of a sandbox.
type PortMapping struct {
// Protocol of the port mapping.
Protocol *Protocol `json:"protocol,omitempty"`
// Port number within the container.
ContainerPort *int32 `json:"container_port,omitempty"`
// Port number on the host.
HostPort *int32 `json:"host_port,omitempty"`
}
// CheckpointData contains all types of data that can be stored in the checkpoint.
type CheckpointData struct {
PortMappings []*PortMapping `json:"port_mappings,omitempty"`
HostNetwork bool `json:"host_network,omitempty"`
}
// PodSandboxCheckpoint is the checkpoint structure for a sandbox
type PodSandboxCheckpoint struct {
// Version of the pod sandbox checkpoint schema.
Version string `json:"version"`
// Pod name of the sandbox. Same as the pod name in the PodSpec.
Name string `json:"name"`
// Pod namespace of the sandbox. Same as the pod namespace in the PodSpec.
Namespace string `json:"namespace"`
// Data to checkpoint for pod sandbox.
Data *CheckpointData `json:"data,omitempty"`
// Checksum is calculated with fnv hash of the checkpoint object with checksum field set to be zero
CheckSum uint64 `json:"checksum"`
}
// CheckpointHandler provides the interface to manage PodSandbox checkpoint
type CheckpointHandler interface {
// CreateCheckpoint persists sandbox checkpoint in CheckpointStore.
CreateCheckpoint(podSandboxID string, checkpoint *PodSandboxCheckpoint) error
// GetCheckpoint retrieves sandbox checkpoint from CheckpointStore.
GetCheckpoint(podSandboxID string) (*PodSandboxCheckpoint, error)
// RemoveCheckpoint removes sandbox checkpoint form CheckpointStore.
// WARNING: RemoveCheckpoint will not return error if checkpoint does not exist.
RemoveCheckpoint(podSandboxID string) error
// ListCheckpoint returns the list of existing checkpoints.
ListCheckpoints() ([]string, error)
}
// PersistentCheckpointHandler is an implementation of CheckpointHandler. It persists checkpoint in CheckpointStore
type PersistentCheckpointHandler struct {
store utilstore.Store
}
func NewPersistentCheckpointHandler(dockershimRootDir string) (CheckpointHandler, error) {
fstore, err := utilstore.NewFileStore(filepath.Join(dockershimRootDir, sandboxCheckpointDir), utilfs.DefaultFs{})
if err != nil {
return nil, err
}
return &PersistentCheckpointHandler{store: fstore}, nil
}
func (handler *PersistentCheckpointHandler) CreateCheckpoint(podSandboxID string, checkpoint *PodSandboxCheckpoint) error {
checkpoint.CheckSum = calculateChecksum(*checkpoint)
blob, err := json.Marshal(checkpoint)
if err != nil {
return err
}
return handler.store.Write(podSandboxID, blob)
}
func (handler *PersistentCheckpointHandler) GetCheckpoint(podSandboxID string) (*PodSandboxCheckpoint, error) {
blob, err := handler.store.Read(podSandboxID)
if err != nil {
return nil, err
}
var checkpoint PodSandboxCheckpoint
//TODO: unmarhsal into a struct with just Version, check version, unmarshal into versioned type.
err = json.Unmarshal(blob, &checkpoint)
if err != nil {
glog.Errorf("Failed to unmarshal checkpoint %q, removing checkpoint. Checkpoint content: %q. ErrMsg: %v", podSandboxID, string(blob), err)
handler.RemoveCheckpoint(podSandboxID)
return nil, fmt.Errorf("failed to unmarshal checkpoint")
}
if checkpoint.CheckSum != calculateChecksum(checkpoint) {
glog.Errorf("Checksum of checkpoint %q is not valid, removing checkpoint", podSandboxID)
handler.RemoveCheckpoint(podSandboxID)
return nil, fmt.Errorf("checkpoint is corrupted")
}
return &checkpoint, nil
}
func (handler *PersistentCheckpointHandler) RemoveCheckpoint(podSandboxID string) error {
return handler.store.Delete(podSandboxID)
}
func (handler *PersistentCheckpointHandler) ListCheckpoints() ([]string, error) {
keys, err := handler.store.List()
if err != nil {
return []string{}, fmt.Errorf("failed to list checkpoint store: %v", err)
}
return keys, nil
}
func NewPodSandboxCheckpoint(namespace, name string) *PodSandboxCheckpoint {
return &PodSandboxCheckpoint{
Version: schemaVersion,
Namespace: namespace,
Name: name,
Data: &CheckpointData{},
}
}
func calculateChecksum(checkpoint PodSandboxCheckpoint) uint64 {
checkpoint.CheckSum = 0
hash := fnv.New32a()
hashutil.DeepHashObject(hash, checkpoint)
return uint64(hash.Sum32())
}

View File

@ -0,0 +1,102 @@
/*
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 dockershim
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
utilstore "k8s.io/kubernetes/pkg/kubelet/dockershim/testing"
)
func NewTestPersistentCheckpointHandler() CheckpointHandler {
return &PersistentCheckpointHandler{store: utilstore.NewMemStore()}
}
func TestPersistentCheckpointHandler(t *testing.T) {
var err error
handler := NewTestPersistentCheckpointHandler()
port80 := int32(80)
port443 := int32(443)
proto := protocolTCP
checkpoint1 := NewPodSandboxCheckpoint("ns1", "sandbox1")
checkpoint1.Data.PortMappings = []*PortMapping{
{
&proto,
&port80,
&port80,
},
{
&proto,
&port443,
&port443,
},
}
checkpoint1.Data.HostNetwork = true
checkpoints := []struct {
podSandboxID string
checkpoint *PodSandboxCheckpoint
expectHostNetwork bool
}{
{
"id1",
checkpoint1,
true,
},
{
"id2",
NewPodSandboxCheckpoint("ns2", "sandbox2"),
false,
},
}
for _, tc := range checkpoints {
// Test CreateCheckpoints
err = handler.CreateCheckpoint(tc.podSandboxID, tc.checkpoint)
assert.NoError(t, err)
// Test GetCheckpoints
checkpoint, err := handler.GetCheckpoint(tc.podSandboxID)
assert.NoError(t, err)
assert.Equal(t, *checkpoint, *tc.checkpoint)
assert.Equal(t, checkpoint.Data.HostNetwork, tc.expectHostNetwork)
}
// Test ListCheckpoints
keys, err := handler.ListCheckpoints()
assert.NoError(t, err)
sort.Strings(keys)
assert.Equal(t, keys, []string{"id1", "id2"})
// Test RemoveCheckpoints
err = handler.RemoveCheckpoint("id1")
assert.NoError(t, err)
// Test Remove Nonexisted Checkpoints
err = handler.RemoveCheckpoint("id1")
assert.NoError(t, err)
// Test ListCheckpoints
keys, err = handler.ListCheckpoints()
assert.NoError(t, err)
assert.Equal(t, keys, []string{"id2"})
// Test Get NonExisted Checkpoint
_, err = handler.GetCheckpoint("id1")
assert.Error(t, err)
}

View File

@ -0,0 +1,413 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"os"
"path/filepath"
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerfilters "github.com/docker/docker/api/types/filters"
dockerstrslice "github.com/docker/docker/api/types/strslice"
"github.com/golang/glog"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
// ListContainers lists all containers matching the filter.
func (ds *dockerService) ListContainers(filter *runtimeapi.ContainerFilter) ([]*runtimeapi.Container, error) {
opts := dockertypes.ContainerListOptions{All: true}
opts.Filters = dockerfilters.NewArgs()
f := newDockerFilter(&opts.Filters)
// Add filter to get *only* (non-sandbox) containers.
f.AddLabel(containerTypeLabelKey, containerTypeLabelContainer)
if filter != nil {
if filter.Id != "" {
f.Add("id", filter.Id)
}
if filter.State != nil {
f.Add("status", toDockerContainerStatus(filter.GetState().State))
}
if filter.PodSandboxId != "" {
f.AddLabel(sandboxIDLabelKey, filter.PodSandboxId)
}
if filter.LabelSelector != nil {
for k, v := range filter.LabelSelector {
f.AddLabel(k, v)
}
}
}
containers, err := ds.client.ListContainers(opts)
if err != nil {
return nil, err
}
// Convert docker to runtime api containers.
result := []*runtimeapi.Container{}
for i := range containers {
c := containers[i]
converted, err := toRuntimeAPIContainer(&c)
if err != nil {
glog.V(4).Infof("Unable to convert docker to runtime API container: %v", err)
continue
}
result = append(result, converted)
}
return result, nil
}
// CreateContainer creates a new container in the given PodSandbox
// Docker cannot store the log to an arbitrary location (yet), so we create an
// symlink at LogPath, linking to the actual path of the log.
// TODO: check if the default values returned by the runtime API are ok.
func (ds *dockerService) CreateContainer(podSandboxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
if config == nil {
return "", fmt.Errorf("container config is nil")
}
if sandboxConfig == nil {
return "", fmt.Errorf("sandbox config is nil for container %q", config.Metadata.Name)
}
labels := makeLabels(config.GetLabels(), config.GetAnnotations())
// Apply a the container type label.
labels[containerTypeLabelKey] = containerTypeLabelContainer
// Write the container log path in the labels.
labels[containerLogPathLabelKey] = filepath.Join(sandboxConfig.LogDirectory, config.LogPath)
// Write the sandbox ID in the labels.
labels[sandboxIDLabelKey] = podSandboxID
apiVersion, err := ds.getDockerAPIVersion()
if err != nil {
return "", fmt.Errorf("unable to get the docker API version: %v", err)
}
securityOptSep := getSecurityOptSeparator(apiVersion)
image := ""
if iSpec := config.GetImage(); iSpec != nil {
image = iSpec.Image
}
createConfig := dockertypes.ContainerCreateConfig{
Name: makeContainerName(sandboxConfig, config),
Config: &dockercontainer.Config{
// TODO: set User.
Entrypoint: dockerstrslice.StrSlice(config.Command),
Cmd: dockerstrslice.StrSlice(config.Args),
Env: generateEnvList(config.GetEnvs()),
Image: image,
WorkingDir: config.WorkingDir,
Labels: labels,
// Interactive containers:
OpenStdin: config.Stdin,
StdinOnce: config.StdinOnce,
Tty: config.Tty,
// Disable Docker's health check until we officially support it
// (https://github.com/kubernetes/kubernetes/issues/25829).
Healthcheck: &dockercontainer.HealthConfig{
Test: []string{"NONE"},
},
},
HostConfig: &dockercontainer.HostConfig{
Binds: generateMountBindings(config.GetMounts()),
},
}
hc := createConfig.HostConfig
ds.updateCreateConfig(&createConfig, config, sandboxConfig, podSandboxID, securityOptSep, apiVersion)
// Set devices for container.
devices := make([]dockercontainer.DeviceMapping, len(config.Devices))
for i, device := range config.Devices {
devices[i] = dockercontainer.DeviceMapping{
PathOnHost: device.HostPath,
PathInContainer: device.ContainerPath,
CgroupPermissions: device.Permissions,
}
}
hc.Resources.Devices = devices
securityOpts, err := ds.getSecurityOpts(config.GetLinux().GetSecurityContext().GetSeccompProfilePath(), securityOptSep)
if err != nil {
return "", fmt.Errorf("failed to generate security options for container %q: %v", config.Metadata.Name, err)
}
hc.SecurityOpt = append(hc.SecurityOpt, securityOpts...)
createResp, err := ds.client.CreateContainer(createConfig)
if err != nil {
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, createConfig, err)
}
if createResp != nil {
return createResp.ID, err
}
return "", err
}
// getContainerLogPath returns the container log path specified by kubelet and the real
// path where docker stores the container log.
func (ds *dockerService) getContainerLogPath(containerID string) (string, string, error) {
info, err := ds.client.InspectContainer(containerID)
if err != nil {
return "", "", fmt.Errorf("failed to inspect container %q: %v", containerID, err)
}
return info.Config.Labels[containerLogPathLabelKey], info.LogPath, nil
}
// createContainerLogSymlink creates the symlink for docker container log.
func (ds *dockerService) createContainerLogSymlink(containerID string) error {
path, realPath, err := ds.getContainerLogPath(containerID)
if err != nil {
return fmt.Errorf("failed to get container %q log path: %v", containerID, err)
}
if path == "" {
glog.V(5).Infof("Container %s log path isn't specified, will not create the symlink", containerID)
return nil
}
if realPath != "" {
// Only create the symlink when container log path is specified and log file exists.
// Delete possibly existing file first
if err = ds.os.Remove(path); err == nil {
glog.Warningf("Deleted previously existing symlink file: %q", path)
}
if err = ds.os.Symlink(realPath, path); err != nil {
return fmt.Errorf("failed to create symbolic link %q to the container log file %q for container %q: %v",
path, realPath, containerID, err)
}
} else {
supported, err := ds.IsCRISupportedLogDriver()
if err != nil {
glog.Warningf("Failed to check supported logging driver by CRI: %v", err)
return nil
}
if supported {
glog.Warningf("Cannot create symbolic link because container log file doesn't exist!")
} else {
glog.V(5).Infof("Unsupported logging driver by CRI")
}
}
return nil
}
// removeContainerLogSymlink removes the symlink for docker container log.
func (ds *dockerService) removeContainerLogSymlink(containerID string) error {
path, _, err := ds.getContainerLogPath(containerID)
if err != nil {
return fmt.Errorf("failed to get container %q log path: %v", containerID, err)
}
if path != "" {
// Only remove the symlink when container log path is specified.
err := ds.os.Remove(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove container %q log symlink %q: %v", containerID, path, err)
}
}
return nil
}
// StartContainer starts the container.
func (ds *dockerService) StartContainer(containerID string) error {
err := ds.client.StartContainer(containerID)
// Create container log symlink for all containers (including failed ones).
if linkError := ds.createContainerLogSymlink(containerID); linkError != nil {
// Do not stop the container if we failed to create symlink because:
// 1. This is not a critical failure.
// 2. We don't have enough information to properly stop container here.
// Kubelet will surface this error to user via an event.
return linkError
}
if err != nil {
err = transformStartContainerError(err)
return fmt.Errorf("failed to start container %q: %v", containerID, err)
}
return nil
}
// StopContainer stops a running container with a grace period (i.e., timeout).
func (ds *dockerService) StopContainer(containerID string, timeout int64) error {
return ds.client.StopContainer(containerID, time.Duration(timeout)*time.Second)
}
// RemoveContainer removes the container.
func (ds *dockerService) RemoveContainer(containerID string) error {
// Ideally, log lifecycle should be independent of container lifecycle.
// However, docker will remove container log after container is removed,
// we can't prevent that now, so we also clean up the symlink here.
err := ds.removeContainerLogSymlink(containerID)
if err != nil {
return err
}
err = ds.client.RemoveContainer(containerID, dockertypes.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
if err != nil {
return fmt.Errorf("failed to remove container %q: %v", containerID, err)
}
return nil
}
func getContainerTimestamps(r *dockertypes.ContainerJSON) (time.Time, time.Time, time.Time, error) {
var createdAt, startedAt, finishedAt time.Time
var err error
createdAt, err = libdocker.ParseDockerTimestamp(r.Created)
if err != nil {
return createdAt, startedAt, finishedAt, err
}
startedAt, err = libdocker.ParseDockerTimestamp(r.State.StartedAt)
if err != nil {
return createdAt, startedAt, finishedAt, err
}
finishedAt, err = libdocker.ParseDockerTimestamp(r.State.FinishedAt)
if err != nil {
return createdAt, startedAt, finishedAt, err
}
return createdAt, startedAt, finishedAt, nil
}
// ContainerStatus inspects the docker container and returns the status.
func (ds *dockerService) ContainerStatus(containerID string) (*runtimeapi.ContainerStatus, error) {
r, err := ds.client.InspectContainer(containerID)
if err != nil {
return nil, err
}
// Parse the timestamps.
createdAt, startedAt, finishedAt, err := getContainerTimestamps(r)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp for container %q: %v", containerID, err)
}
// Convert the image id to a pullable id.
ir, err := ds.client.InspectImageByID(r.Image)
if err != nil {
return nil, fmt.Errorf("unable to inspect docker image %q while inspecting docker container %q: %v", r.Image, containerID, err)
}
imageID := toPullableImageID(r.Image, ir)
// Convert the mounts.
mounts := make([]*runtimeapi.Mount, 0, len(r.Mounts))
for i := range r.Mounts {
m := r.Mounts[i]
readonly := !m.RW
mounts = append(mounts, &runtimeapi.Mount{
HostPath: m.Source,
ContainerPath: m.Destination,
Readonly: readonly,
// Note: Can't set SeLinuxRelabel
})
}
// Interpret container states.
var state runtimeapi.ContainerState
var reason, message string
if r.State.Running {
// Container is running.
state = runtimeapi.ContainerState_CONTAINER_RUNNING
} else {
// Container is *not* running. We need to get more details.
// * Case 1: container has run and exited with non-zero finishedAt
// time.
// * Case 2: container has failed to start; it has a zero finishedAt
// time, but a non-zero exit code.
// * Case 3: container has been created, but not started (yet).
if !finishedAt.IsZero() { // Case 1
state = runtimeapi.ContainerState_CONTAINER_EXITED
switch {
case r.State.OOMKilled:
// TODO: consider exposing OOMKilled via the runtimeAPI.
// Note: if an application handles OOMKilled gracefully, the
// exit code could be zero.
reason = "OOMKilled"
case r.State.ExitCode == 0:
reason = "Completed"
default:
reason = "Error"
}
} else if r.State.ExitCode != 0 { // Case 2
state = runtimeapi.ContainerState_CONTAINER_EXITED
// Adjust finshedAt and startedAt time to createdAt time to avoid
// the confusion.
finishedAt, startedAt = createdAt, createdAt
reason = "ContainerCannotRun"
} else { // Case 3
state = runtimeapi.ContainerState_CONTAINER_CREATED
}
message = r.State.Error
}
// Convert to unix timestamps.
ct, st, ft := createdAt.UnixNano(), startedAt.UnixNano(), finishedAt.UnixNano()
exitCode := int32(r.State.ExitCode)
metadata, err := parseContainerName(r.Name)
if err != nil {
return nil, err
}
labels, annotations := extractLabels(r.Config.Labels)
imageName := r.Config.Image
if len(ir.RepoTags) > 0 {
imageName = ir.RepoTags[0]
}
return &runtimeapi.ContainerStatus{
Id: r.ID,
Metadata: metadata,
Image: &runtimeapi.ImageSpec{Image: imageName},
ImageRef: imageID,
Mounts: mounts,
ExitCode: exitCode,
State: state,
CreatedAt: ct,
StartedAt: st,
FinishedAt: ft,
Reason: reason,
Message: message,
Labels: labels,
Annotations: annotations,
LogPath: r.Config.Labels[containerLogPathLabelKey],
}, nil
}
func (ds *dockerService) UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources) error {
updateConfig := dockercontainer.UpdateConfig{
Resources: dockercontainer.Resources{
CPUPeriod: resources.CpuPeriod,
CPUQuota: resources.CpuQuota,
CPUShares: resources.CpuShares,
Memory: resources.MemoryLimitInBytes,
CpusetCpus: resources.CpusetCpus,
CpusetMems: resources.CpusetMems,
},
}
err := ds.client.UpdateContainerResources(containerID, updateConfig)
if err != nil {
return fmt.Errorf("failed to update container %q: %v", containerID, err)
}
return nil
}

View File

@ -0,0 +1,292 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"path/filepath"
"strings"
"testing"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
containertest "k8s.io/kubernetes/pkg/kubelet/container/testing"
)
// A helper to create a basic config.
func makeContainerConfig(sConfig *runtimeapi.PodSandboxConfig, name, image string, attempt uint32, labels, annotations map[string]string) *runtimeapi.ContainerConfig {
return &runtimeapi.ContainerConfig{
Metadata: &runtimeapi.ContainerMetadata{
Name: name,
Attempt: attempt,
},
Image: &runtimeapi.ImageSpec{Image: image},
Labels: labels,
Annotations: annotations,
}
}
// TestListContainers creates several containers and then list them to check
// whether the correct metadatas, states, and labels are returned.
func TestListContainers(t *testing.T) {
ds, _, fakeClock := newTestDockerService()
podName, namespace := "foo", "bar"
containerName, image := "sidecar", "logger"
configs := []*runtimeapi.ContainerConfig{}
sConfigs := []*runtimeapi.PodSandboxConfig{}
for i := 0; i < 3; i++ {
s := makeSandboxConfig(fmt.Sprintf("%s%d", podName, i),
fmt.Sprintf("%s%d", namespace, i), fmt.Sprintf("%d", i), 0)
labels := map[string]string{"abc.xyz": fmt.Sprintf("label%d", i)}
annotations := map[string]string{"foo.bar.baz": fmt.Sprintf("annotation%d", i)}
c := makeContainerConfig(s, fmt.Sprintf("%s%d", containerName, i),
fmt.Sprintf("%s:v%d", image, i), uint32(i), labels, annotations)
sConfigs = append(sConfigs, s)
configs = append(configs, c)
}
expected := []*runtimeapi.Container{}
state := runtimeapi.ContainerState_CONTAINER_RUNNING
var createdAt int64 = fakeClock.Now().UnixNano()
for i := range configs {
// We don't care about the sandbox id; pass a bogus one.
sandboxID := fmt.Sprintf("sandboxid%d", i)
id, err := ds.CreateContainer(sandboxID, configs[i], sConfigs[i])
assert.NoError(t, err)
err = ds.StartContainer(id)
assert.NoError(t, err)
imageRef := "" // FakeDockerClient doesn't populate ImageRef yet.
// Prepend to the expected list because ListContainers returns
// the most recent containers first.
expected = append([]*runtimeapi.Container{{
Metadata: configs[i].Metadata,
Id: id,
PodSandboxId: sandboxID,
State: state,
CreatedAt: createdAt,
Image: configs[i].Image,
ImageRef: imageRef,
Labels: configs[i].Labels,
Annotations: configs[i].Annotations,
}}, expected...)
}
containers, err := ds.ListContainers(nil)
assert.NoError(t, err)
assert.Len(t, containers, len(expected))
assert.Equal(t, expected, containers)
}
// TestContainerStatus tests the basic lifecycle operations and verify that
// the status returned reflects the operations performed.
func TestContainerStatus(t *testing.T) {
ds, fDocker, fClock := newTestDockerService()
sConfig := makeSandboxConfig("foo", "bar", "1", 0)
labels := map[string]string{"abc.xyz": "foo"}
annotations := map[string]string{"foo.bar.baz": "abc"}
imageName := "iamimage"
config := makeContainerConfig(sConfig, "pause", imageName, 0, labels, annotations)
var defaultTime time.Time
dt := defaultTime.UnixNano()
ct, st, ft := dt, dt, dt
state := runtimeapi.ContainerState_CONTAINER_CREATED
imageRef := DockerImageIDPrefix + imageName
// The following variables are not set in FakeDockerClient.
exitCode := int32(0)
var reason, message string
expected := &runtimeapi.ContainerStatus{
State: state,
CreatedAt: ct,
StartedAt: st,
FinishedAt: ft,
Metadata: config.Metadata,
Image: config.Image,
ImageRef: imageRef,
ExitCode: exitCode,
Reason: reason,
Message: message,
Mounts: []*runtimeapi.Mount{},
Labels: config.Labels,
Annotations: config.Annotations,
}
fDocker.InjectImages([]dockertypes.ImageSummary{{ID: imageName}})
// Create the container.
fClock.SetTime(time.Now().Add(-1 * time.Hour))
expected.CreatedAt = fClock.Now().UnixNano()
const sandboxId = "sandboxid"
id, err := ds.CreateContainer(sandboxId, config, sConfig)
assert.NoError(t, err)
// Check internal labels
c, err := fDocker.InspectContainer(id)
assert.NoError(t, err)
assert.Equal(t, c.Config.Labels[containerTypeLabelKey], containerTypeLabelContainer)
assert.Equal(t, c.Config.Labels[sandboxIDLabelKey], sandboxId)
// Set the id manually since we don't know the id until it's created.
expected.Id = id
assert.NoError(t, err)
status, err := ds.ContainerStatus(id)
assert.NoError(t, err)
assert.Equal(t, expected, status)
// Advance the clock and start the container.
fClock.SetTime(time.Now())
expected.StartedAt = fClock.Now().UnixNano()
expected.State = runtimeapi.ContainerState_CONTAINER_RUNNING
err = ds.StartContainer(id)
assert.NoError(t, err)
status, err = ds.ContainerStatus(id)
assert.Equal(t, expected, status)
// Advance the clock and stop the container.
fClock.SetTime(time.Now().Add(1 * time.Hour))
expected.FinishedAt = fClock.Now().UnixNano()
expected.State = runtimeapi.ContainerState_CONTAINER_EXITED
expected.Reason = "Completed"
err = ds.StopContainer(id, 0)
assert.NoError(t, err)
status, err = ds.ContainerStatus(id)
assert.Equal(t, expected, status)
// Remove the container.
err = ds.RemoveContainer(id)
assert.NoError(t, err)
status, err = ds.ContainerStatus(id)
assert.Error(t, err, fmt.Sprintf("status of container: %+v", status))
}
// TestContainerLogPath tests the container log creation logic.
func TestContainerLogPath(t *testing.T) {
ds, fDocker, _ := newTestDockerService()
podLogPath := "/pod/1"
containerLogPath := "0"
kubeletContainerLogPath := filepath.Join(podLogPath, containerLogPath)
sConfig := makeSandboxConfig("foo", "bar", "1", 0)
sConfig.LogDirectory = podLogPath
config := makeContainerConfig(sConfig, "pause", "iamimage", 0, nil, nil)
config.LogPath = containerLogPath
const sandboxId = "sandboxid"
id, err := ds.CreateContainer(sandboxId, config, sConfig)
// Check internal container log label
c, err := fDocker.InspectContainer(id)
assert.NoError(t, err)
assert.Equal(t, c.Config.Labels[containerLogPathLabelKey], kubeletContainerLogPath)
// Set docker container log path
dockerContainerLogPath := "/docker/container/log"
c.LogPath = dockerContainerLogPath
// Verify container log symlink creation
fakeOS := ds.os.(*containertest.FakeOS)
fakeOS.SymlinkFn = func(oldname, newname string) error {
assert.Equal(t, dockerContainerLogPath, oldname)
assert.Equal(t, kubeletContainerLogPath, newname)
return nil
}
err = ds.StartContainer(id)
assert.NoError(t, err)
err = ds.StopContainer(id, 0)
assert.NoError(t, err)
// Verify container log symlink deletion
// symlink is also tentatively deleted at startup
err = ds.RemoveContainer(id)
assert.NoError(t, err)
assert.Equal(t, []string{kubeletContainerLogPath, kubeletContainerLogPath}, fakeOS.Removes)
}
// TestContainerCreationConflict tests the logic to work around docker container
// creation naming conflict bug.
func TestContainerCreationConflict(t *testing.T) {
sConfig := makeSandboxConfig("foo", "bar", "1", 0)
config := makeContainerConfig(sConfig, "pause", "iamimage", 0, map[string]string{}, map[string]string{})
containerName := makeContainerName(sConfig, config)
const sandboxId = "sandboxid"
const containerId = "containerid"
conflictError := fmt.Errorf("Error response from daemon: Conflict. The name \"/%s\" is already in use by container %s. You have to remove (or rename) that container to be able to reuse that name.",
containerName, containerId)
noContainerError := fmt.Errorf("Error response from daemon: No such container: %s", containerId)
randomError := fmt.Errorf("random error")
for desc, test := range map[string]struct {
createError error
removeError error
expectError error
expectCalls []string
expectFields int
}{
"no create error": {
expectCalls: []string{"create"},
expectFields: 6,
},
"random create error": {
createError: randomError,
expectError: randomError,
expectCalls: []string{"create"},
},
"conflict create error with successful remove": {
createError: conflictError,
expectError: conflictError,
expectCalls: []string{"create", "remove"},
},
"conflict create error with random remove error": {
createError: conflictError,
removeError: randomError,
expectError: conflictError,
expectCalls: []string{"create", "remove"},
},
"conflict create error with no such container remove error": {
createError: conflictError,
removeError: noContainerError,
expectCalls: []string{"create", "remove", "create"},
expectFields: 7,
},
} {
t.Logf("TestCase: %s", desc)
ds, fDocker, _ := newTestDockerService()
if test.createError != nil {
fDocker.InjectError("create", test.createError)
}
if test.removeError != nil {
fDocker.InjectError("remove", test.removeError)
}
id, err := ds.CreateContainer(sandboxId, config, sConfig)
require.Equal(t, test.expectError, err)
assert.NoError(t, fDocker.AssertCalls(test.expectCalls))
if err == nil {
c, err := fDocker.InspectContainer(id)
assert.NoError(t, err)
assert.Len(t, strings.Split(c.Name, nameDelimiter), test.expectFields)
}
}
}

View File

@ -0,0 +1,151 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"net/http"
dockertypes "github.com/docker/docker/api/types"
dockerfilters "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/jsonmessage"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
// This file implements methods in ImageManagerService.
// ListImages lists existing images.
func (ds *dockerService) ListImages(filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error) {
opts := dockertypes.ImageListOptions{}
if filter != nil {
if filter.GetImage().GetImage() != "" {
opts.Filters = dockerfilters.NewArgs()
opts.Filters.Add("reference", filter.GetImage().GetImage())
}
}
images, err := ds.client.ListImages(opts)
if err != nil {
return nil, err
}
result := make([]*runtimeapi.Image, 0, len(images))
for _, i := range images {
apiImage, err := imageToRuntimeAPIImage(&i)
if err != nil {
// TODO: log an error message?
continue
}
result = append(result, apiImage)
}
return result, nil
}
// ImageStatus returns the status of the image, returns nil if the image doesn't present.
func (ds *dockerService) ImageStatus(image *runtimeapi.ImageSpec) (*runtimeapi.Image, error) {
imageInspect, err := ds.client.InspectImageByRef(image.Image)
if err != nil {
if libdocker.IsImageNotFoundError(err) {
return nil, nil
}
return nil, err
}
return imageInspectToRuntimeAPIImage(imageInspect)
}
// PullImage pulls an image with authentication config.
func (ds *dockerService) PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig) (string, error) {
authConfig := dockertypes.AuthConfig{}
if auth != nil {
authConfig.Username = auth.Username
authConfig.Password = auth.Password
authConfig.ServerAddress = auth.ServerAddress
authConfig.IdentityToken = auth.IdentityToken
authConfig.RegistryToken = auth.RegistryToken
}
err := ds.client.PullImage(image.Image,
authConfig,
dockertypes.ImagePullOptions{},
)
if err != nil {
return "", filterHTTPError(err, image.Image)
}
return getImageRef(ds.client, image.Image)
}
// RemoveImage removes the image.
func (ds *dockerService) RemoveImage(image *runtimeapi.ImageSpec) error {
// If the image has multiple tags, we need to remove all the tags
// TODO: We assume image.Image is image ID here, which is true in the current implementation
// of kubelet, but we should still clarify this in CRI.
imageInspect, err := ds.client.InspectImageByID(image.Image)
if err == nil && imageInspect != nil && len(imageInspect.RepoTags) > 1 {
for _, tag := range imageInspect.RepoTags {
if _, err := ds.client.RemoveImage(tag, dockertypes.ImageRemoveOptions{PruneChildren: true}); err != nil && !libdocker.IsImageNotFoundError(err) {
return err
}
}
return nil
}
// dockerclient.InspectImageByID doesn't work with digest and repoTags,
// it is safe to continue removing it since there is another check below.
if err != nil && !libdocker.IsImageNotFoundError(err) {
return err
}
_, err = ds.client.RemoveImage(image.Image, dockertypes.ImageRemoveOptions{PruneChildren: true})
if err != nil && !libdocker.IsImageNotFoundError(err) {
return err
}
return nil
}
// getImageRef returns the image digest if exists, or else returns the image ID.
func getImageRef(client libdocker.Interface, image string) (string, error) {
img, err := client.InspectImageByRef(image)
if err != nil {
return "", err
}
if img == nil {
return "", fmt.Errorf("unable to inspect image %s", image)
}
// Returns the digest if it exist.
if len(img.RepoDigests) > 0 {
return img.RepoDigests[0], nil
}
return img.ID, nil
}
func filterHTTPError(err error, image string) error {
// docker/docker/pull/11314 prints detailed error info for docker pull.
// When it hits 502, it returns a verbose html output including an inline svg,
// which makes the output of kubectl get pods much harder to parse.
// Here converts such verbose output to a concise one.
jerr, ok := err.(*jsonmessage.JSONError)
if ok && (jerr.Code == http.StatusBadGateway ||
jerr.Code == http.StatusServiceUnavailable ||
jerr.Code == http.StatusGatewayTimeout) {
return fmt.Errorf("RegistryUnavailable: %v", err)
}
return err
}

View File

@ -0,0 +1,30 @@
// +build linux
/*
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 dockershim
import (
"fmt"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ImageFsInfo returns information of the filesystem that is used to store images.
func (ds *dockerService) ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@ -0,0 +1,74 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"testing"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/stretchr/testify/assert"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
func TestRemoveImage(t *testing.T) {
ds, fakeDocker, _ := newTestDockerService()
id := "1111"
fakeDocker.InjectImageInspects([]dockertypes.ImageInspect{{ID: id, RepoTags: []string{"foo"}}})
ds.RemoveImage(&runtimeapi.ImageSpec{Image: id})
fakeDocker.AssertCallDetails(libdocker.NewCalledDetail("inspect_image", nil),
libdocker.NewCalledDetail("remove_image", []interface{}{id, dockertypes.ImageRemoveOptions{PruneChildren: true}}))
}
func TestRemoveImageWithMultipleTags(t *testing.T) {
ds, fakeDocker, _ := newTestDockerService()
id := "1111"
fakeDocker.InjectImageInspects([]dockertypes.ImageInspect{{ID: id, RepoTags: []string{"foo", "bar"}}})
ds.RemoveImage(&runtimeapi.ImageSpec{Image: id})
fakeDocker.AssertCallDetails(libdocker.NewCalledDetail("inspect_image", nil),
libdocker.NewCalledDetail("remove_image", []interface{}{"foo", dockertypes.ImageRemoveOptions{PruneChildren: true}}),
libdocker.NewCalledDetail("remove_image", []interface{}{"bar", dockertypes.ImageRemoveOptions{PruneChildren: true}}))
}
func TestPullWithJSONError(t *testing.T) {
ds, fakeDocker, _ := newTestDockerService()
tests := map[string]struct {
image *runtimeapi.ImageSpec
err error
expectedError string
}{
"Json error": {
&runtimeapi.ImageSpec{Image: "ubuntu"},
&jsonmessage.JSONError{Code: 50, Message: "Json error"},
"Json error",
},
"Bad gateway": {
&runtimeapi.ImageSpec{Image: "ubuntu"},
&jsonmessage.JSONError{Code: 502, Message: "<!doctype html>\n<html class=\"no-js\" lang=\"\">\n <head>\n </head>\n <body>\n <h1>Oops, there was an error!</h1>\n <p>We have been contacted of this error, feel free to check out <a href=\"http://status.docker.com/\">status.docker.com</a>\n to see if there is a bigger issue.</p>\n\n </body>\n</html>"},
"RegistryUnavailable",
},
}
for key, test := range tests {
fakeDocker.InjectError("pull", test.err)
_, err := ds.PullImage(test.image, &runtimeapi.AuthConfig{})
assert.Error(t, err, fmt.Sprintf("TestCase [%s]", key))
assert.Contains(t, err.Error(), test.expectedError)
}
}

View File

@ -0,0 +1,30 @@
// +build !linux,!windows
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ImageFsInfo returns information of the filesystem that is used to store images.
func (ds *dockerService) ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@ -0,0 +1,39 @@
// +build windows
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"time"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ImageFsInfo returns information of the filesystem that is used to store images.
func (ds *dockerService) ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error) {
// For Windows Stats to work correctly, a file system must be provided. For now, provide a fake filesystem.
filesystems := []*runtimeapi.FilesystemUsage{
{
Timestamp: time.Now().UnixNano(),
UsedBytes: &runtimeapi.UInt64Value{Value: 0},
InodesUsed: &runtimeapi.UInt64Value{Value: 0},
},
}
return filesystems, nil
}

View File

@ -0,0 +1,677 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"os"
"strings"
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerfilters "github.com/docker/docker/api/types/filters"
"github.com/golang/glog"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/kubelet/qos"
"k8s.io/kubernetes/pkg/kubelet/types"
)
const (
defaultSandboxImage = "gcr.io/google_containers/pause-amd64:3.0"
// Various default sandbox resources requests/limits.
defaultSandboxCPUshares int64 = 2
// Name of the underlying container runtime
runtimeName = "docker"
)
var (
// Termination grace period
defaultSandboxGracePeriod = time.Duration(10) * time.Second
)
// Returns whether the sandbox network is ready, and whether the sandbox is known
func (ds *dockerService) getNetworkReady(podSandboxID string) (bool, bool) {
ds.networkReadyLock.Lock()
defer ds.networkReadyLock.Unlock()
ready, ok := ds.networkReady[podSandboxID]
return ready, ok
}
func (ds *dockerService) setNetworkReady(podSandboxID string, ready bool) {
ds.networkReadyLock.Lock()
defer ds.networkReadyLock.Unlock()
ds.networkReady[podSandboxID] = ready
}
func (ds *dockerService) clearNetworkReady(podSandboxID string) {
ds.networkReadyLock.Lock()
defer ds.networkReadyLock.Unlock()
delete(ds.networkReady, podSandboxID)
}
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
// For docker, PodSandbox is implemented by a container holding the network
// namespace for the pod.
// Note: docker doesn't use LogDirectory (yet).
func (ds *dockerService) RunPodSandbox(config *runtimeapi.PodSandboxConfig) (id string, err error) {
// Step 1: Pull the image for the sandbox.
image := defaultSandboxImage
podSandboxImage := ds.podSandboxImage
if len(podSandboxImage) != 0 {
image = podSandboxImage
}
// NOTE: To use a custom sandbox image in a private repository, users need to configure the nodes with credentials properly.
// see: http://kubernetes.io/docs/user-guide/images/#configuring-nodes-to-authenticate-to-a-private-repository
// Only pull sandbox image when it's not present - v1.PullIfNotPresent.
if err := ensureSandboxImageExists(ds.client, image); err != nil {
return "", err
}
// Step 2: Create the sandbox container.
createConfig, err := ds.makeSandboxDockerConfig(config, image)
if err != nil {
return "", fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
}
createResp, err := ds.client.CreateContainer(*createConfig)
if err != nil {
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
}
if err != nil || createResp == nil {
return "", fmt.Errorf("failed to create a sandbox for pod %q: %v", config.Metadata.Name, err)
}
ds.setNetworkReady(createResp.ID, false)
defer func(e *error) {
// Set networking ready depending on the error return of
// the parent function
if *e == nil {
ds.setNetworkReady(createResp.ID, true)
}
}(&err)
// Step 3: Create Sandbox Checkpoint.
if err = ds.checkpointHandler.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
return createResp.ID, err
}
// Step 4: Start the sandbox container.
// Assume kubelet's garbage collector would remove the sandbox later, if
// startContainer failed.
err = ds.client.StartContainer(createResp.ID)
if err != nil {
return createResp.ID, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
}
// Rewrite resolv.conf file generated by docker.
// NOTE: cluster dns settings aren't passed anymore to docker api in all cases,
// not only for pods with host network: the resolver conf will be overwritten
// after sandbox creation to override docker's behaviour. This resolv.conf
// file is shared by all containers of the same pod, and needs to be modified
// only once per pod.
if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
containerInfo, err := ds.client.InspectContainer(createResp.ID)
if err != nil {
return createResp.ID, fmt.Errorf("failed to inspect sandbox container for pod %q: %v", config.Metadata.Name, err)
}
if err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options); err != nil {
return createResp.ID, fmt.Errorf("rewrite resolv.conf failed for pod %q: %v", config.Metadata.Name, err)
}
}
// Do not invoke network plugins if in hostNetwork mode.
if nsOptions := config.GetLinux().GetSecurityContext().GetNamespaceOptions(); nsOptions != nil && nsOptions.HostNetwork {
return createResp.ID, nil
}
// Step 5: Setup networking for the sandbox.
// All pod networking is setup by a CNI plugin discovered at startup time.
// This plugin assigns the pod ip, sets up routes inside the sandbox,
// creates interfaces etc. In theory, its jurisdiction ends with pod
// sandbox networking, but it might insert iptables rules or open ports
// on the host as well, to satisfy parts of the pod spec that aren't
// recognized by the CNI standard yet.
cID := kubecontainer.BuildContainerID(runtimeName, createResp.ID)
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations)
if err != nil {
// TODO(random-liu): Do we need to teardown network here?
if err := ds.client.StopContainer(createResp.ID, defaultSandboxGracePeriod); err != nil {
glog.Warningf("Failed to stop sandbox container %q for pod %q: %v", createResp.ID, config.Metadata.Name, err)
}
}
return createResp.ID, err
}
// StopPodSandbox stops the sandbox. If there are any running containers in the
// sandbox, they should be force terminated.
// TODO: This function blocks sandbox teardown on networking teardown. Is it
// better to cut our losses assuming an out of band GC routine will cleanup
// after us?
func (ds *dockerService) StopPodSandbox(podSandboxID string) error {
var namespace, name string
var hostNetwork bool
var checkpointErr, statusErr error
// Try to retrieve sandbox information from docker daemon or sandbox checkpoint
status, statusErr := ds.PodSandboxStatus(podSandboxID)
if statusErr == nil {
nsOpts := status.GetLinux().GetNamespaces().GetOptions()
hostNetwork = nsOpts != nil && nsOpts.HostNetwork
m := status.GetMetadata()
namespace = m.Namespace
name = m.Name
} else {
var checkpoint *PodSandboxCheckpoint
checkpoint, checkpointErr = ds.checkpointHandler.GetCheckpoint(podSandboxID)
// Proceed if both sandbox container and checkpoint could not be found. This means that following
// actions will only have sandbox ID and not have pod namespace and name information.
// Return error if encounter any unexpected error.
if checkpointErr != nil {
if libdocker.IsContainerNotFoundError(statusErr) {
glog.Warningf("Both sandbox container and checkpoint for id %q could not be found. "+
"Proceed without further sandbox information.", podSandboxID)
} else {
return utilerrors.NewAggregate([]error{
fmt.Errorf("failed to get checkpoint for sandbox %q: %v", podSandboxID, checkpointErr),
fmt.Errorf("failed to get sandbox status: %v", statusErr)})
}
} else {
namespace = checkpoint.Namespace
name = checkpoint.Name
hostNetwork = checkpoint.Data != nil && checkpoint.Data.HostNetwork
}
}
// WARNING: The following operations made the following assumption:
// 1. kubelet will retry on any error returned by StopPodSandbox.
// 2. tearing down network and stopping sandbox container can succeed in any sequence.
// This depends on the implementation detail of network plugin and proper error handling.
// For kubenet, if tearing down network failed and sandbox container is stopped, kubelet
// will retry. On retry, kubenet will not be able to retrieve network namespace of the sandbox
// since it is stopped. With empty network namespcae, CNI bridge plugin will conduct best
// effort clean up and will not return error.
errList := []error{}
ready, ok := ds.getNetworkReady(podSandboxID)
if !hostNetwork && (ready || !ok) {
// Only tear down the pod network if we haven't done so already
cID := kubecontainer.BuildContainerID(runtimeName, podSandboxID)
err := ds.network.TearDownPod(namespace, name, cID)
if err == nil {
ds.setNetworkReady(podSandboxID, false)
} else {
errList = append(errList, err)
}
}
if err := ds.client.StopContainer(podSandboxID, defaultSandboxGracePeriod); err != nil {
// Do not return error if the container does not exist
if !libdocker.IsContainerNotFoundError(err) {
glog.Errorf("Failed to stop sandbox %q: %v", podSandboxID, err)
errList = append(errList, err)
} else {
// remove the checkpoint for any sandbox that is not found in the runtime
ds.checkpointHandler.RemoveCheckpoint(podSandboxID)
}
}
return utilerrors.NewAggregate(errList)
// TODO: Stop all running containers in the sandbox.
}
// RemovePodSandbox removes the sandbox. If there are running containers in the
// sandbox, they should be forcibly removed.
func (ds *dockerService) RemovePodSandbox(podSandboxID string) error {
var errs []error
opts := dockertypes.ContainerListOptions{All: true}
opts.Filters = dockerfilters.NewArgs()
f := newDockerFilter(&opts.Filters)
f.AddLabel(sandboxIDLabelKey, podSandboxID)
containers, err := ds.client.ListContainers(opts)
if err != nil {
errs = append(errs, err)
}
// Remove all containers in the sandbox.
for i := range containers {
if err := ds.RemoveContainer(containers[i].ID); err != nil && !libdocker.IsContainerNotFoundError(err) {
errs = append(errs, err)
}
}
// Remove the sandbox container.
err = ds.client.RemoveContainer(podSandboxID, dockertypes.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
if err == nil || libdocker.IsContainerNotFoundError(err) {
// Only clear network ready when the sandbox has actually been
// removed from docker or doesn't exist
ds.clearNetworkReady(podSandboxID)
} else {
errs = append(errs, err)
}
// Remove the checkpoint of the sandbox.
if err := ds.checkpointHandler.RemoveCheckpoint(podSandboxID); err != nil {
errs = append(errs, err)
}
return utilerrors.NewAggregate(errs)
}
// getIPFromPlugin interrogates the network plugin for an IP.
func (ds *dockerService) getIPFromPlugin(sandbox *dockertypes.ContainerJSON) (string, error) {
metadata, err := parseSandboxName(sandbox.Name)
if err != nil {
return "", err
}
msg := fmt.Sprintf("Couldn't find network status for %s/%s through plugin", metadata.Namespace, metadata.Name)
cID := kubecontainer.BuildContainerID(runtimeName, sandbox.ID)
networkStatus, err := ds.network.GetPodNetworkStatus(metadata.Namespace, metadata.Name, cID)
if err != nil {
return "", err
}
if networkStatus == nil {
return "", fmt.Errorf("%v: invalid network status for", msg)
}
return networkStatus.IP.String(), nil
}
// getIP returns the ip given the output of `docker inspect` on a pod sandbox,
// first interrogating any registered plugins, then simply trusting the ip
// in the sandbox itself. We look for an ipv4 address before ipv6.
func (ds *dockerService) getIP(podSandboxID string, sandbox *dockertypes.ContainerJSON) string {
if sandbox.NetworkSettings == nil {
return ""
}
if sharesHostNetwork(sandbox) {
// For sandboxes using host network, the shim is not responsible for
// reporting the IP.
return ""
}
// Don't bother getting IP if the pod is known and networking isn't ready
ready, ok := ds.getNetworkReady(podSandboxID)
if ok && !ready {
return ""
}
ip, err := ds.getIPFromPlugin(sandbox)
if err == nil {
return ip
}
// TODO: trusting the docker ip is not a great idea. However docker uses
// eth0 by default and so does CNI, so if we find a docker IP here, we
// conclude that the plugin must have failed setup, or forgotten its ip.
// This is not a sensible assumption for plugins across the board, but if
// a plugin doesn't want this behavior, it can throw an error.
if sandbox.NetworkSettings.IPAddress != "" {
return sandbox.NetworkSettings.IPAddress
}
if sandbox.NetworkSettings.GlobalIPv6Address != "" {
return sandbox.NetworkSettings.GlobalIPv6Address
}
// If all else fails, warn but don't return an error, as pod status
// should generally not return anything except fatal errors
// FIXME: handle network errors by restarting the pod somehow?
glog.Warningf("failed to read pod IP from plugin/docker: %v", err)
return ""
}
// PodSandboxStatus returns the status of the PodSandbox.
func (ds *dockerService) PodSandboxStatus(podSandboxID string) (*runtimeapi.PodSandboxStatus, error) {
// Inspect the container.
r, err := ds.client.InspectContainer(podSandboxID)
if err != nil {
return nil, err
}
// Parse the timestamps.
createdAt, _, _, err := getContainerTimestamps(r)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp for container %q: %v", podSandboxID, err)
}
ct := createdAt.UnixNano()
// Translate container to sandbox state.
state := runtimeapi.PodSandboxState_SANDBOX_NOTREADY
if r.State.Running {
state = runtimeapi.PodSandboxState_SANDBOX_READY
}
var IP string
// TODO: Remove this when sandbox is available on windows
// This is a workaround for windows, where sandbox is not in use, and pod IP is determined through containers belonging to the Pod.
if IP = ds.determinePodIPBySandboxID(podSandboxID); IP == "" {
IP = ds.getIP(podSandboxID, r)
}
hostNetwork := sharesHostNetwork(r)
metadata, err := parseSandboxName(r.Name)
if err != nil {
return nil, err
}
labels, annotations := extractLabels(r.Config.Labels)
return &runtimeapi.PodSandboxStatus{
Id: r.ID,
State: state,
CreatedAt: ct,
Metadata: metadata,
Labels: labels,
Annotations: annotations,
Network: &runtimeapi.PodSandboxNetworkStatus{
Ip: IP,
},
Linux: &runtimeapi.LinuxPodSandboxStatus{
Namespaces: &runtimeapi.Namespace{
Options: &runtimeapi.NamespaceOption{
HostNetwork: hostNetwork,
HostPid: sharesHostPid(r),
HostIpc: sharesHostIpc(r),
},
},
},
}, nil
}
// ListPodSandbox returns a list of Sandbox.
func (ds *dockerService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) {
// By default, list all containers whether they are running or not.
opts := dockertypes.ContainerListOptions{All: true}
filterOutReadySandboxes := false
opts.Filters = dockerfilters.NewArgs()
f := newDockerFilter(&opts.Filters)
// Add filter to select only sandbox containers.
f.AddLabel(containerTypeLabelKey, containerTypeLabelSandbox)
if filter != nil {
if filter.Id != "" {
f.Add("id", filter.Id)
}
if filter.State != nil {
if filter.GetState().State == runtimeapi.PodSandboxState_SANDBOX_READY {
// Only list running containers.
opts.All = false
} else {
// runtimeapi.PodSandboxState_SANDBOX_NOTREADY can mean the
// container is in any of the non-running state (e.g., created,
// exited). We can't tell docker to filter out running
// containers directly, so we'll need to filter them out
// ourselves after getting the results.
filterOutReadySandboxes = true
}
}
if filter.LabelSelector != nil {
for k, v := range filter.LabelSelector {
f.AddLabel(k, v)
}
}
}
// Make sure we get the list of checkpoints first so that we don't include
// new PodSandboxes that are being created right now.
var err error
checkpoints := []string{}
if filter == nil {
checkpoints, err = ds.checkpointHandler.ListCheckpoints()
if err != nil {
glog.Errorf("Failed to list checkpoints: %v", err)
}
}
containers, err := ds.client.ListContainers(opts)
if err != nil {
return nil, err
}
// Convert docker containers to runtime api sandboxes.
result := []*runtimeapi.PodSandbox{}
// using map as set
sandboxIDs := make(map[string]bool)
for i := range containers {
c := containers[i]
converted, err := containerToRuntimeAPISandbox(&c)
if err != nil {
glog.V(4).Infof("Unable to convert docker to runtime API sandbox %+v: %v", c, err)
continue
}
if filterOutReadySandboxes && converted.State == runtimeapi.PodSandboxState_SANDBOX_READY {
continue
}
sandboxIDs[converted.Id] = true
result = append(result, converted)
}
// Include sandbox that could only be found with its checkpoint if no filter is applied
// These PodSandbox will only include PodSandboxID, Name, Namespace.
// These PodSandbox will be in PodSandboxState_SANDBOX_NOTREADY state.
for _, id := range checkpoints {
if _, ok := sandboxIDs[id]; ok {
continue
}
checkpoint, err := ds.checkpointHandler.GetCheckpoint(id)
if err != nil {
glog.Errorf("Failed to retrieve checkpoint for sandbox %q: %v", id, err)
continue
}
result = append(result, checkpointToRuntimeAPISandbox(id, checkpoint))
}
return result, nil
}
// applySandboxLinuxOptions applies LinuxPodSandboxConfig to dockercontainer.HostConfig and dockercontainer.ContainerCreateConfig.
func (ds *dockerService) applySandboxLinuxOptions(hc *dockercontainer.HostConfig, lc *runtimeapi.LinuxPodSandboxConfig, createConfig *dockertypes.ContainerCreateConfig, image string, separator rune) error {
if lc == nil {
return nil
}
// Apply security context.
if err := applySandboxSecurityContext(lc, createConfig.Config, hc, ds.network, separator); err != nil {
return err
}
// Set sysctls.
hc.Sysctls = lc.Sysctls
return nil
}
func (ds *dockerService) applySandboxResources(hc *dockercontainer.HostConfig, lc *runtimeapi.LinuxPodSandboxConfig) error {
hc.Resources = dockercontainer.Resources{
MemorySwap: DefaultMemorySwap(),
CPUShares: defaultSandboxCPUshares,
// Use docker's default cpu quota/period.
}
if lc != nil {
// Apply Cgroup options.
cgroupParent, err := ds.GenerateExpectedCgroupParent(lc.CgroupParent)
if err != nil {
return err
}
hc.CgroupParent = cgroupParent
}
return nil
}
// makeSandboxDockerConfig returns dockertypes.ContainerCreateConfig based on runtimeapi.PodSandboxConfig.
func (ds *dockerService) makeSandboxDockerConfig(c *runtimeapi.PodSandboxConfig, image string) (*dockertypes.ContainerCreateConfig, error) {
// Merge annotations and labels because docker supports only labels.
labels := makeLabels(c.GetLabels(), c.GetAnnotations())
// Apply a label to distinguish sandboxes from regular containers.
labels[containerTypeLabelKey] = containerTypeLabelSandbox
// Apply a container name label for infra container. This is used in summary v1.
// TODO(random-liu): Deprecate this label once container metrics is directly got from CRI.
labels[types.KubernetesContainerNameLabel] = sandboxContainerName
apiVersion, err := ds.getDockerAPIVersion()
if err != nil {
return nil, fmt.Errorf("unable to get the docker API version: %v", err)
}
securityOptSep := getSecurityOptSeparator(apiVersion)
hc := &dockercontainer.HostConfig{}
createConfig := &dockertypes.ContainerCreateConfig{
Name: makeSandboxName(c),
Config: &dockercontainer.Config{
Hostname: c.Hostname,
// TODO: Handle environment variables.
Image: image,
Labels: labels,
},
HostConfig: hc,
}
// Apply linux-specific options.
if err := ds.applySandboxLinuxOptions(hc, c.GetLinux(), createConfig, image, securityOptSep); err != nil {
return nil, err
}
// Set port mappings.
exposedPorts, portBindings := makePortsAndBindings(c.GetPortMappings())
createConfig.Config.ExposedPorts = exposedPorts
hc.PortBindings = portBindings
// TODO: Get rid of the dependency on kubelet internal package.
hc.OomScoreAdj = qos.PodInfraOOMAdj
// Apply resource options.
if err := ds.applySandboxResources(hc, c.GetLinux()); err != nil {
return nil, err
}
// Set security options.
securityOpts, err := ds.getSecurityOpts(c.GetLinux().GetSecurityContext().GetSeccompProfilePath(), securityOptSep)
if err != nil {
return nil, fmt.Errorf("failed to generate sandbox security options for sandbox %q: %v", c.Metadata.Name, err)
}
hc.SecurityOpt = append(hc.SecurityOpt, securityOpts...)
return createConfig, nil
}
// sharesHostNetwork returns true if the given container is sharing the host's
// network namespace.
func sharesHostNetwork(container *dockertypes.ContainerJSON) bool {
if container != nil && container.HostConfig != nil {
return string(container.HostConfig.NetworkMode) == namespaceModeHost
}
return false
}
// sharesHostPid returns true if the given container is sharing the host's pid
// namespace.
func sharesHostPid(container *dockertypes.ContainerJSON) bool {
if container != nil && container.HostConfig != nil {
return string(container.HostConfig.PidMode) == namespaceModeHost
}
return false
}
// sharesHostIpc returns true if the given container is sharing the host's ipc
// namespace.
func sharesHostIpc(container *dockertypes.ContainerJSON) bool {
if container != nil && container.HostConfig != nil {
return string(container.HostConfig.IpcMode) == namespaceModeHost
}
return false
}
func constructPodSandboxCheckpoint(config *runtimeapi.PodSandboxConfig) *PodSandboxCheckpoint {
checkpoint := NewPodSandboxCheckpoint(config.Metadata.Namespace, config.Metadata.Name)
for _, pm := range config.GetPortMappings() {
proto := toCheckpointProtocol(pm.Protocol)
checkpoint.Data.PortMappings = append(checkpoint.Data.PortMappings, &PortMapping{
HostPort: &pm.HostPort,
ContainerPort: &pm.ContainerPort,
Protocol: &proto,
})
}
if nsOptions := config.GetLinux().GetSecurityContext().GetNamespaceOptions(); nsOptions != nil {
checkpoint.Data.HostNetwork = nsOptions.HostNetwork
}
return checkpoint
}
func toCheckpointProtocol(protocol runtimeapi.Protocol) Protocol {
switch protocol {
case runtimeapi.Protocol_TCP:
return protocolTCP
case runtimeapi.Protocol_UDP:
return protocolUDP
}
glog.Warningf("Unknown protocol %q: defaulting to TCP", protocol)
return protocolTCP
}
// rewriteResolvFile rewrites resolv.conf file generated by docker.
func rewriteResolvFile(resolvFilePath string, dns []string, dnsSearch []string, dnsOptions []string) error {
if len(resolvFilePath) == 0 {
glog.Errorf("ResolvConfPath is empty.")
return nil
}
if _, err := os.Stat(resolvFilePath); os.IsNotExist(err) {
return fmt.Errorf("ResolvConfPath %q does not exist", resolvFilePath)
}
var resolvFileContent []string
for _, srv := range dns {
resolvFileContent = append(resolvFileContent, "nameserver "+srv)
}
if len(dnsSearch) > 0 {
resolvFileContent = append(resolvFileContent, "search "+strings.Join(dnsSearch, " "))
}
if len(dnsOptions) > 0 {
resolvFileContent = append(resolvFileContent, "options "+strings.Join(dnsOptions, " "))
}
if len(resolvFileContent) > 0 {
resolvFileContentStr := strings.Join(resolvFileContent, "\n")
resolvFileContentStr += "\n"
glog.V(4).Infof("Will attempt to re-write config file %s with: \n%s", resolvFilePath, resolvFileContent)
if err := rewriteFile(resolvFilePath, resolvFileContentStr); err != nil {
glog.Errorf("resolv.conf could not be updated: %v", err)
return err
}
}
return nil
}
func rewriteFile(filePath, stringToWrite string) error {
f, err := os.OpenFile(filePath, os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(stringToWrite)
return err
}

View File

@ -0,0 +1,294 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"errors"
"fmt"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/kubelet/network"
"k8s.io/kubernetes/pkg/kubelet/types"
)
// A helper to create a basic config.
func makeSandboxConfig(name, namespace, uid string, attempt uint32) *runtimeapi.PodSandboxConfig {
return makeSandboxConfigWithLabelsAndAnnotations(name, namespace, uid, attempt, map[string]string{}, map[string]string{})
}
func makeSandboxConfigWithLabelsAndAnnotations(name, namespace, uid string, attempt uint32, labels, annotations map[string]string) *runtimeapi.PodSandboxConfig {
return &runtimeapi.PodSandboxConfig{
Metadata: &runtimeapi.PodSandboxMetadata{
Name: name,
Namespace: namespace,
Uid: uid,
Attempt: attempt,
},
Labels: labels,
Annotations: annotations,
}
}
// TestListSandboxes creates several sandboxes and then list them to check
// whether the correct metadatas, states, and labels are returned.
func TestListSandboxes(t *testing.T) {
ds, _, fakeClock := newTestDockerService()
name, namespace := "foo", "bar"
configs := []*runtimeapi.PodSandboxConfig{}
for i := 0; i < 3; i++ {
c := makeSandboxConfigWithLabelsAndAnnotations(fmt.Sprintf("%s%d", name, i),
fmt.Sprintf("%s%d", namespace, i), fmt.Sprintf("%d", i), 0,
map[string]string{"label": fmt.Sprintf("foo%d", i)},
map[string]string{"annotation": fmt.Sprintf("bar%d", i)},
)
configs = append(configs, c)
}
expected := []*runtimeapi.PodSandbox{}
state := runtimeapi.PodSandboxState_SANDBOX_READY
var createdAt int64 = fakeClock.Now().UnixNano()
for i := range configs {
id, err := ds.RunPodSandbox(configs[i])
assert.NoError(t, err)
// Prepend to the expected list because ListPodSandbox returns
// the most recent sandbox first.
expected = append([]*runtimeapi.PodSandbox{{
Metadata: configs[i].Metadata,
Id: id,
State: state,
CreatedAt: createdAt,
Labels: configs[i].Labels,
Annotations: configs[i].Annotations,
}}, expected...)
}
sandboxes, err := ds.ListPodSandbox(nil)
assert.NoError(t, err)
assert.Len(t, sandboxes, len(expected))
assert.Equal(t, expected, sandboxes)
}
// TestSandboxStatus tests the basic lifecycle operations and verify that
// the status returned reflects the operations performed.
func TestSandboxStatus(t *testing.T) {
ds, fDocker, fClock := newTestDockerService()
labels := map[string]string{"label": "foobar1"}
annotations := map[string]string{"annotation": "abc"}
config := makeSandboxConfigWithLabelsAndAnnotations("foo", "bar", "1", 0, labels, annotations)
// TODO: The following variables depend on the internal
// implementation of FakeDockerClient, and should be fixed.
fakeIP := "2.3.4.5"
state := runtimeapi.PodSandboxState_SANDBOX_READY
ct := int64(0)
hostNetwork := false
expected := &runtimeapi.PodSandboxStatus{
State: state,
CreatedAt: ct,
Metadata: config.Metadata,
Network: &runtimeapi.PodSandboxNetworkStatus{Ip: fakeIP},
Linux: &runtimeapi.LinuxPodSandboxStatus{Namespaces: &runtimeapi.Namespace{Options: &runtimeapi.NamespaceOption{HostNetwork: hostNetwork}}},
Labels: labels,
Annotations: annotations,
}
// Create the sandbox.
fClock.SetTime(time.Now())
expected.CreatedAt = fClock.Now().UnixNano()
id, err := ds.RunPodSandbox(config)
// Check internal labels
c, err := fDocker.InspectContainer(id)
assert.NoError(t, err)
assert.Equal(t, c.Config.Labels[containerTypeLabelKey], containerTypeLabelSandbox)
assert.Equal(t, c.Config.Labels[types.KubernetesContainerNameLabel], sandboxContainerName)
expected.Id = id // ID is only known after the creation.
status, err := ds.PodSandboxStatus(id)
assert.NoError(t, err)
assert.Equal(t, expected, status)
// Stop the sandbox.
expected.State = runtimeapi.PodSandboxState_SANDBOX_NOTREADY
err = ds.StopPodSandbox(id)
assert.NoError(t, err)
// IP not valid after sandbox stop
expected.Network.Ip = ""
status, err = ds.PodSandboxStatus(id)
assert.Equal(t, expected, status)
// Remove the container.
err = ds.RemovePodSandbox(id)
assert.NoError(t, err)
status, err = ds.PodSandboxStatus(id)
assert.Error(t, err, fmt.Sprintf("status of sandbox: %+v", status))
}
// TestSandboxStatusAfterRestart tests that retrieving sandbox status returns
// an IP address even if RunPodSandbox() was not yet called for this pod, as
// would happen on kubelet restart
func TestSandboxStatusAfterRestart(t *testing.T) {
ds, _, fClock := newTestDockerService()
config := makeSandboxConfig("foo", "bar", "1", 0)
// TODO: The following variables depend on the internal
// implementation of FakeDockerClient, and should be fixed.
fakeIP := "2.3.4.5"
state := runtimeapi.PodSandboxState_SANDBOX_READY
ct := int64(0)
hostNetwork := false
expected := &runtimeapi.PodSandboxStatus{
State: state,
CreatedAt: ct,
Metadata: config.Metadata,
Network: &runtimeapi.PodSandboxNetworkStatus{Ip: fakeIP},
Linux: &runtimeapi.LinuxPodSandboxStatus{Namespaces: &runtimeapi.Namespace{Options: &runtimeapi.NamespaceOption{HostNetwork: hostNetwork}}},
Labels: map[string]string{},
Annotations: map[string]string{},
}
// Create the sandbox.
fClock.SetTime(time.Now())
expected.CreatedAt = fClock.Now().UnixNano()
createConfig, err := ds.makeSandboxDockerConfig(config, defaultSandboxImage)
assert.NoError(t, err)
createResp, err := ds.client.CreateContainer(*createConfig)
assert.NoError(t, err)
err = ds.client.StartContainer(createResp.ID)
assert.NoError(t, err)
// Check status without RunPodSandbox() having set up networking
expected.Id = createResp.ID // ID is only known after the creation.
status, err := ds.PodSandboxStatus(createResp.ID)
assert.NoError(t, err)
assert.Equal(t, expected, status)
}
// TestNetworkPluginInvocation checks that the right SetUpPod and TearDownPod
// calls are made when we run/stop a sandbox.
func TestNetworkPluginInvocation(t *testing.T) {
ds, _, _ := newTestDockerService()
mockPlugin := newTestNetworkPlugin(t)
ds.network = network.NewPluginManager(mockPlugin)
defer mockPlugin.Finish()
name := "foo0"
ns := "bar0"
c := makeSandboxConfigWithLabelsAndAnnotations(
name, ns, "0", 0,
map[string]string{"label": name},
map[string]string{"annotation": ns},
)
cID := kubecontainer.ContainerID{Type: runtimeName, ID: libdocker.GetFakeContainerID(fmt.Sprintf("/%v", makeSandboxName(c)))}
mockPlugin.EXPECT().Name().Return("mockNetworkPlugin").AnyTimes()
setup := mockPlugin.EXPECT().SetUpPod(ns, name, cID)
// StopPodSandbox performs a lookup on status to figure out if the sandbox
// is running with hostnetworking, as all its given is the ID.
mockPlugin.EXPECT().GetPodNetworkStatus(ns, name, cID)
mockPlugin.EXPECT().TearDownPod(ns, name, cID).After(setup)
_, err := ds.RunPodSandbox(c)
assert.NoError(t, err)
err = ds.StopPodSandbox(cID.ID)
assert.NoError(t, err)
}
// TestHostNetworkPluginInvocation checks that *no* SetUp/TearDown calls happen
// for host network sandboxes.
func TestHostNetworkPluginInvocation(t *testing.T) {
ds, _, _ := newTestDockerService()
mockPlugin := newTestNetworkPlugin(t)
ds.network = network.NewPluginManager(mockPlugin)
defer mockPlugin.Finish()
name := "foo0"
ns := "bar0"
c := makeSandboxConfigWithLabelsAndAnnotations(
name, ns, "0", 0,
map[string]string{"label": name},
map[string]string{"annotation": ns},
)
hostNetwork := true
c.Linux = &runtimeapi.LinuxPodSandboxConfig{
SecurityContext: &runtimeapi.LinuxSandboxSecurityContext{
NamespaceOptions: &runtimeapi.NamespaceOption{
HostNetwork: hostNetwork,
},
},
}
cID := kubecontainer.ContainerID{Type: runtimeName, ID: libdocker.GetFakeContainerID(fmt.Sprintf("/%v", makeSandboxName(c)))}
// No calls to network plugin are expected
_, err := ds.RunPodSandbox(c)
assert.NoError(t, err)
assert.NoError(t, ds.StopPodSandbox(cID.ID))
}
// TestSetUpPodFailure checks that the sandbox should be not ready when it
// hits a SetUpPod failure.
func TestSetUpPodFailure(t *testing.T) {
ds, _, _ := newTestDockerService()
mockPlugin := newTestNetworkPlugin(t)
ds.network = network.NewPluginManager(mockPlugin)
defer mockPlugin.Finish()
name := "foo0"
ns := "bar0"
c := makeSandboxConfigWithLabelsAndAnnotations(
name, ns, "0", 0,
map[string]string{"label": name},
map[string]string{"annotation": ns},
)
cID := kubecontainer.ContainerID{Type: runtimeName, ID: libdocker.GetFakeContainerID(fmt.Sprintf("/%v", makeSandboxName(c)))}
mockPlugin.EXPECT().Name().Return("mockNetworkPlugin").AnyTimes()
mockPlugin.EXPECT().SetUpPod(ns, name, cID).Return(errors.New("setup pod error")).AnyTimes()
// Assume network plugin doesn't return error, dockershim should still be able to return not ready correctly.
mockPlugin.EXPECT().GetPodNetworkStatus(ns, name, cID).Return(&network.PodNetworkStatus{IP: net.IP("127.0.0.01")}, nil).AnyTimes()
t.Logf("RunPodSandbox should return error")
_, err := ds.RunPodSandbox(c)
assert.Error(t, err)
t.Logf("PodSandboxStatus should be not ready")
status, err := ds.PodSandboxStatus(cID.ID)
assert.NoError(t, err)
assert.Equal(t, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, status.State)
t.Logf("ListPodSandbox should also show not ready")
sandboxes, err := ds.ListPodSandbox(nil)
assert.NoError(t, err)
var sandbox *runtimeapi.PodSandbox
for _, s := range sandboxes {
if s.Id == cID.ID {
sandbox = s
break
}
}
assert.NotNil(t, sandbox)
assert.Equal(t, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, sandbox.State)
}

View File

@ -0,0 +1,609 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/armon/circbuf"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubetypes "k8s.io/apimachinery/pkg/types"
internalapi "k8s.io/kubernetes/pkg/kubelet/apis/cri"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig"
kubecm "k8s.io/kubernetes/pkg/kubelet/cm"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/cm"
"k8s.io/kubernetes/pkg/kubelet/network"
"k8s.io/kubernetes/pkg/kubelet/network/cni"
"k8s.io/kubernetes/pkg/kubelet/network/hostport"
"k8s.io/kubernetes/pkg/kubelet/network/kubenet"
"k8s.io/kubernetes/pkg/kubelet/server/streaming"
"k8s.io/kubernetes/pkg/kubelet/util/cache"
utilstore "k8s.io/kubernetes/pkg/kubelet/util/store"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/kubelet/dockershim/metrics"
)
const (
dockerRuntimeName = "docker"
kubeAPIVersion = "0.1.0"
// String used to detect docker host mode for various namespaces (e.g.
// networking). Must match the value returned by docker inspect -f
// '{{.HostConfig.NetworkMode}}'.
namespaceModeHost = "host"
dockerNetNSFmt = "/proc/%v/ns/net"
defaultSeccompProfile = "unconfined"
// Internal docker labels used to identify whether a container is a sandbox
// or a regular container.
// TODO: This is not backward compatible with older containers. We will
// need to add filtering based on names.
containerTypeLabelKey = "io.kubernetes.docker.type"
containerTypeLabelSandbox = "podsandbox"
containerTypeLabelContainer = "container"
containerLogPathLabelKey = "io.kubernetes.container.logpath"
sandboxIDLabelKey = "io.kubernetes.sandbox.id"
// The expiration time of version cache.
versionCacheTTL = 60 * time.Second
defaultCgroupDriver = "cgroupfs"
// TODO: https://github.com/kubernetes/kubernetes/pull/31169 provides experimental
// defaulting of host user namespace that may be enabled when the docker daemon
// is using remapped UIDs.
// Dockershim should provide detection support for a remapping environment .
// This should be included in the feature proposal. Defaulting may still occur according
// to kubelet behavior and system settings in addition to any API flags that may be introduced.
)
// NetworkPluginSettings is the subset of kubelet runtime args we pass
// to the container runtime shim so it can probe for network plugins.
// In the future we will feed these directly to a standalone container
// runtime process.
type NetworkPluginSettings struct {
// HairpinMode is best described by comments surrounding the kubelet arg
HairpinMode kubeletconfig.HairpinMode
// NonMasqueradeCIDR is the range of ips which should *not* be included
// in any MASQUERADE rules applied by the plugin
NonMasqueradeCIDR string
// PluginName is the name of the plugin, runtime shim probes for
PluginName string
// PluginBinDir is the directory in which the binaries for the plugin with
// PluginName is kept. The admin is responsible for provisioning these
// binaries before-hand.
PluginBinDir string
// PluginConfDir is the directory in which the admin places a CNI conf.
// Depending on the plugin, this may be an optional field, eg: kubenet
// generates its own plugin conf.
PluginConfDir string
// MTU is the desired MTU for network devices created by the plugin.
MTU int
// RuntimeHost is an interface that serves as a trap-door from plugin back
// into the kubelet.
// TODO: This shouldn't be required, remove once we move host ports into CNI
// and figure out bandwidth shaping. See corresponding comments above
// network.Host interface.
LegacyRuntimeHost network.LegacyHost
}
// namespaceGetter is a wrapper around the dockerService that implements
// the network.NamespaceGetter interface.
type namespaceGetter struct {
ds *dockerService
}
func (n *namespaceGetter) GetNetNS(containerID string) (string, error) {
return n.ds.GetNetNS(containerID)
}
// portMappingGetter is a wrapper around the dockerService that implements
// the network.PortMappingGetter interface.
type portMappingGetter struct {
ds *dockerService
}
func (p *portMappingGetter) GetPodPortMappings(containerID string) ([]*hostport.PortMapping, error) {
return p.ds.GetPodPortMappings(containerID)
}
// dockerNetworkHost implements network.Host by wrapping the legacy host passed in by the kubelet
// and dockerServices which implementes the rest of the network host interfaces.
// The legacy host methods are slated for deletion.
type dockerNetworkHost struct {
network.LegacyHost
*namespaceGetter
*portMappingGetter
}
var internalLabelKeys []string = []string{containerTypeLabelKey, containerLogPathLabelKey, sandboxIDLabelKey}
// ClientConfig is parameters used to initialize docker client
type ClientConfig struct {
DockerEndpoint string
RuntimeRequestTimeout time.Duration
ImagePullProgressDeadline time.Duration
// Configuration for fake docker client
EnableSleep bool
WithTraceDisabled bool
}
// NewDockerClientFromConfig create a docker client from given configure
// return nil if nil configure is given.
func NewDockerClientFromConfig(config *ClientConfig) libdocker.Interface {
if config != nil {
// Create docker client.
client := libdocker.ConnectToDockerOrDie(
config.DockerEndpoint,
config.RuntimeRequestTimeout,
config.ImagePullProgressDeadline,
config.WithTraceDisabled,
config.EnableSleep,
)
return client
}
return nil
}
// NOTE: Anything passed to DockerService should be eventually handled in another way when we switch to running the shim as a different process.
func NewDockerService(config *ClientConfig, podSandboxImage string, streamingConfig *streaming.Config,
pluginSettings *NetworkPluginSettings, cgroupsName string, kubeCgroupDriver string, dockershimRootDir string, disableSharedPID bool) (DockerService, error) {
client := NewDockerClientFromConfig(config)
c := libdocker.NewInstrumentedInterface(client)
checkpointHandler, err := NewPersistentCheckpointHandler(dockershimRootDir)
if err != nil {
return nil, err
}
ds := &dockerService{
client: c,
os: kubecontainer.RealOS{},
podSandboxImage: podSandboxImage,
streamingRuntime: &streamingRuntime{
client: client,
execHandler: &NativeExecHandler{},
},
containerManager: cm.NewContainerManager(cgroupsName, client),
checkpointHandler: checkpointHandler,
disableSharedPID: disableSharedPID,
networkReady: make(map[string]bool),
}
// check docker version compatibility.
if err = ds.checkVersionCompatibility(); err != nil {
return nil, err
}
// create streaming server if configured.
if streamingConfig != nil {
var err error
ds.streamingServer, err = streaming.NewServer(*streamingConfig, ds.streamingRuntime)
if err != nil {
return nil, err
}
}
// dockershim currently only supports CNI plugins.
cniPlugins := cni.ProbeNetworkPlugins(pluginSettings.PluginConfDir, pluginSettings.PluginBinDir)
cniPlugins = append(cniPlugins, kubenet.NewPlugin(pluginSettings.PluginBinDir))
netHost := &dockerNetworkHost{
pluginSettings.LegacyRuntimeHost,
&namespaceGetter{ds},
&portMappingGetter{ds},
}
plug, err := network.InitNetworkPlugin(cniPlugins, pluginSettings.PluginName, netHost, pluginSettings.HairpinMode, pluginSettings.NonMasqueradeCIDR, pluginSettings.MTU)
if err != nil {
return nil, fmt.Errorf("didn't find compatible CNI plugin with given settings %+v: %v", pluginSettings, err)
}
ds.network = network.NewPluginManager(plug)
glog.Infof("Docker cri networking managed by %v", plug.Name())
// NOTE: cgroup driver is only detectable in docker 1.11+
cgroupDriver := defaultCgroupDriver
dockerInfo, err := ds.client.Info()
glog.Infof("Docker Info: %+v", dockerInfo)
if err != nil {
glog.Errorf("Failed to execute Info() call to the Docker client: %v", err)
glog.Warningf("Falling back to use the default driver: %q", cgroupDriver)
} else if len(dockerInfo.CgroupDriver) == 0 {
glog.Warningf("No cgroup driver is set in Docker")
glog.Warningf("Falling back to use the default driver: %q", cgroupDriver)
} else {
cgroupDriver = dockerInfo.CgroupDriver
}
if len(kubeCgroupDriver) != 0 && kubeCgroupDriver != cgroupDriver {
return nil, fmt.Errorf("misconfiguration: kubelet cgroup driver: %q is different from docker cgroup driver: %q", kubeCgroupDriver, cgroupDriver)
}
glog.Infof("Setting cgroupDriver to %s", cgroupDriver)
ds.cgroupDriver = cgroupDriver
ds.versionCache = cache.NewObjectCache(
func() (interface{}, error) {
return ds.getDockerVersion()
},
versionCacheTTL,
)
// Register prometheus metrics.
metrics.Register()
return ds, nil
}
// DockerService is an interface that embeds the new RuntimeService and
// ImageService interfaces.
type DockerService interface {
internalapi.RuntimeService
internalapi.ImageManagerService
Start() error
// For serving streaming calls.
http.Handler
// IsCRISupportedLogDriver checks whether the logging driver used by docker is
// suppoted by native CRI integration.
// TODO(resouer): remove this when deprecating unsupported log driver
IsCRISupportedLogDriver() (bool, error)
// NewDockerLegacyService created docker legacy service when log driver is not supported.
// TODO(resouer): remove this when deprecating unsupported log driver
NewDockerLegacyService() DockerLegacyService
}
type dockerService struct {
client libdocker.Interface
os kubecontainer.OSInterface
podSandboxImage string
streamingRuntime *streamingRuntime
streamingServer streaming.Server
network *network.PluginManager
// Map of podSandboxID :: network-is-ready
networkReady map[string]bool
networkReadyLock sync.Mutex
containerManager cm.ContainerManager
// cgroup driver used by Docker runtime.
cgroupDriver string
checkpointHandler CheckpointHandler
// caches the version of the runtime.
// To be compatible with multiple docker versions, we need to perform
// version checking for some operations. Use this cache to avoid querying
// the docker daemon every time we need to do such checks.
versionCache *cache.ObjectCache
// This option provides an escape hatch to override the new default behavior for Docker under
// the CRI to use a shared PID namespace for all pods. It is temporary and will be removed.
// See proposals/pod-pid-namespace.md for details.
// TODO: Remove once the escape hatch is no longer used (https://issues.k8s.io/41938)
disableSharedPID bool
}
// Version returns the runtime name, runtime version and runtime API version
func (ds *dockerService) Version(_ string) (*runtimeapi.VersionResponse, error) {
v, err := ds.getDockerVersion()
if err != nil {
return nil, err
}
return &runtimeapi.VersionResponse{
Version: kubeAPIVersion,
RuntimeName: dockerRuntimeName,
RuntimeVersion: v.Version,
RuntimeApiVersion: v.APIVersion,
}, nil
}
// dockerVersion gets the version information from docker.
func (ds *dockerService) getDockerVersion() (*dockertypes.Version, error) {
v, err := ds.client.Version()
if err != nil {
return nil, fmt.Errorf("failed to get docker version: %v", err)
}
// Docker API version (e.g., 1.23) is not semver compatible. Add a ".0"
// suffix to remedy this.
v.APIVersion = fmt.Sprintf("%s.0", v.APIVersion)
return v, nil
}
// UpdateRuntimeConfig updates the runtime config. Currently only handles podCIDR updates.
func (ds *dockerService) UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig) (err error) {
if runtimeConfig == nil {
return
}
glog.Infof("docker cri received runtime config %+v", runtimeConfig)
if ds.network != nil && runtimeConfig.NetworkConfig.PodCidr != "" {
event := make(map[string]interface{})
event[network.NET_PLUGIN_EVENT_POD_CIDR_CHANGE_DETAIL_CIDR] = runtimeConfig.NetworkConfig.PodCidr
ds.network.Event(network.NET_PLUGIN_EVENT_POD_CIDR_CHANGE, event)
}
return
}
// GetNetNS returns the network namespace of the given containerID. The ID
// supplied is typically the ID of a pod sandbox. This getter doesn't try
// to map non-sandbox IDs to their respective sandboxes.
func (ds *dockerService) GetNetNS(podSandboxID string) (string, error) {
r, err := ds.client.InspectContainer(podSandboxID)
if err != nil {
return "", err
}
return getNetworkNamespace(r)
}
// GetPodPortMappings returns the port mappings of the given podSandbox ID.
func (ds *dockerService) GetPodPortMappings(podSandboxID string) ([]*hostport.PortMapping, error) {
// TODO: get portmappings from docker labels for backward compatibility
checkpoint, err := ds.checkpointHandler.GetCheckpoint(podSandboxID)
// Return empty portMappings if checkpoint is not found
if err != nil {
if err == utilstore.ErrKeyNotFound {
return nil, nil
}
return nil, err
}
portMappings := make([]*hostport.PortMapping, 0, len(checkpoint.Data.PortMappings))
for _, pm := range checkpoint.Data.PortMappings {
proto := toAPIProtocol(*pm.Protocol)
portMappings = append(portMappings, &hostport.PortMapping{
HostPort: *pm.HostPort,
ContainerPort: *pm.ContainerPort,
Protocol: proto,
})
}
return portMappings, nil
}
// Start initializes and starts components in dockerService.
func (ds *dockerService) Start() error {
// Initialize the legacy cleanup flag.
return ds.containerManager.Start()
}
// Status returns the status of the runtime.
// TODO(random-liu): Set network condition accordingly here.
func (ds *dockerService) Status() (*runtimeapi.RuntimeStatus, error) {
runtimeReady := &runtimeapi.RuntimeCondition{
Type: runtimeapi.RuntimeReady,
Status: true,
}
networkReady := &runtimeapi.RuntimeCondition{
Type: runtimeapi.NetworkReady,
Status: true,
}
conditions := []*runtimeapi.RuntimeCondition{runtimeReady, networkReady}
if _, err := ds.client.Version(); err != nil {
runtimeReady.Status = false
runtimeReady.Reason = "DockerDaemonNotReady"
runtimeReady.Message = fmt.Sprintf("docker: failed to get docker version: %v", err)
}
if err := ds.network.Status(); err != nil {
networkReady.Status = false
networkReady.Reason = "NetworkPluginNotReady"
networkReady.Message = fmt.Sprintf("docker: network plugin is not ready: %v", err)
}
return &runtimeapi.RuntimeStatus{Conditions: conditions}, nil
}
func (ds *dockerService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ds.streamingServer != nil {
ds.streamingServer.ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
}
// GenerateExpectedCgroupParent returns cgroup parent in syntax expected by cgroup driver
func (ds *dockerService) GenerateExpectedCgroupParent(cgroupParent string) (string, error) {
if len(cgroupParent) > 0 {
// if docker uses the systemd cgroup driver, it expects *.slice style names for cgroup parent.
// if we configured kubelet to use --cgroup-driver=cgroupfs, and docker is configured to use systemd driver
// docker will fail to launch the container because the name we provide will not be a valid slice.
// this is a very good thing.
if ds.cgroupDriver == "systemd" {
systemdCgroupParent, err := kubecm.ConvertCgroupFsNameToSystemd(cgroupParent)
if err != nil {
return "", err
}
cgroupParent = systemdCgroupParent
}
}
glog.V(3).Infof("Setting cgroup parent to: %q", cgroupParent)
return cgroupParent, nil
}
// checkVersionCompatibility verifies whether docker is in a compatible version.
func (ds *dockerService) checkVersionCompatibility() error {
apiVersion, err := ds.getDockerAPIVersion()
if err != nil {
return err
}
minAPIVersion, err := semver.Parse(libdocker.MinimumDockerAPIVersion)
if err != nil {
return err
}
// Verify the docker version.
result := apiVersion.Compare(minAPIVersion)
if result < 0 {
return fmt.Errorf("docker API version is older than %s", libdocker.MinimumDockerAPIVersion)
}
return nil
}
// getDockerAPIVersion gets the semver-compatible docker api version.
func (ds *dockerService) getDockerAPIVersion() (*semver.Version, error) {
var dv *dockertypes.Version
var err error
if ds.versionCache != nil {
dv, err = ds.getDockerVersionFromCache()
} else {
dv, err = ds.getDockerVersion()
}
if err != nil {
return nil, err
}
apiVersion, err := semver.Parse(dv.APIVersion)
if err != nil {
return nil, err
}
return &apiVersion, nil
}
func (ds *dockerService) getDockerVersionFromCache() (*dockertypes.Version, error) {
// We only store on key in the cache.
const dummyKey = "version"
value, err := ds.versionCache.Get(dummyKey)
if err != nil {
return nil, err
}
dv, ok := value.(*dockertypes.Version)
if !ok {
return nil, fmt.Errorf("Converted to *dockertype.Version error")
}
return dv, nil
}
func toAPIProtocol(protocol Protocol) v1.Protocol {
switch protocol {
case protocolTCP:
return v1.ProtocolTCP
case protocolUDP:
return v1.ProtocolUDP
}
glog.Warningf("Unknown protocol %q: defaulting to TCP", protocol)
return v1.ProtocolTCP
}
// DockerLegacyService interface embeds some legacy methods for backward compatibility.
type DockerLegacyService interface {
// GetContainerLogs gets logs for a specific container.
GetContainerLogs(*v1.Pod, kubecontainer.ContainerID, *v1.PodLogOptions, io.Writer, io.Writer) error
}
// dockerLegacyService implements the DockerLegacyService. We add this for non json-log driver
// support. (See #41996)
type dockerLegacyService struct {
client libdocker.Interface
}
// NewDockerLegacyService created docker legacy service when log driver is not supported.
// TODO(resouer): remove this when deprecating unsupported log driver
func (d *dockerService) NewDockerLegacyService() DockerLegacyService {
return &dockerLegacyService{client: d.client}
}
// GetContainerLogs get container logs directly from docker daemon.
func (d *dockerLegacyService) GetContainerLogs(pod *v1.Pod, containerID kubecontainer.ContainerID, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error {
container, err := d.client.InspectContainer(containerID.ID)
if err != nil {
return err
}
var since int64
if logOptions.SinceSeconds != nil {
t := metav1.Now().Add(-time.Duration(*logOptions.SinceSeconds) * time.Second)
since = t.Unix()
}
if logOptions.SinceTime != nil {
since = logOptions.SinceTime.Unix()
}
opts := dockertypes.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Since: strconv.FormatInt(since, 10),
Timestamps: logOptions.Timestamps,
Follow: logOptions.Follow,
}
if logOptions.TailLines != nil {
opts.Tail = strconv.FormatInt(*logOptions.TailLines, 10)
}
sopts := libdocker.StreamOptions{
OutputStream: stdout,
ErrorStream: stderr,
RawTerminal: container.Config.Tty,
}
return d.client.Logs(containerID.ID, opts, sopts)
}
// LegacyLogProvider implements the kuberuntime.LegacyLogProvider interface
type LegacyLogProvider struct {
dls DockerLegacyService
}
func NewLegacyLogProvider(dls DockerLegacyService) LegacyLogProvider {
return LegacyLogProvider{dls: dls}
}
// GetContainerLogTail attempts to read up to MaxContainerTerminationMessageLogLength
// from the end of the log when docker is configured with a log driver other than json-log.
// It reads up to MaxContainerTerminationMessageLogLines lines.
func (l LegacyLogProvider) GetContainerLogTail(uid kubetypes.UID, name, namespace string, containerId kubecontainer.ContainerID) (string, error) {
value := int64(kubecontainer.MaxContainerTerminationMessageLogLines)
buf, _ := circbuf.NewBuffer(kubecontainer.MaxContainerTerminationMessageLogLength)
// Although this is not a full spec pod, dockerLegacyService.GetContainerLogs() currently completely ignores its pod param
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
UID: uid,
Name: name,
Namespace: namespace,
},
}
err := l.dls.GetContainerLogs(pod, containerId, &v1.PodLogOptions{TailLines: &value}, buf, buf)
if err != nil {
return "", err
}
return buf.String(), nil
}
// criSupportedLogDrivers are log drivers supported by native CRI integration.
var criSupportedLogDrivers = []string{"json-file"}
// IsCRISupportedLogDriver checks whether the logging driver used by docker is
// suppoted by native CRI integration.
func (d *dockerService) IsCRISupportedLogDriver() (bool, error) {
info, err := d.client.Info()
if err != nil {
return false, fmt.Errorf("failed to get docker info: %v", err)
}
for _, driver := range criSupportedLogDrivers {
if info.LoggingDriver == driver {
return true, nil
}
}
return false, nil
}

View File

@ -0,0 +1,136 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"errors"
"testing"
"time"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/clock"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
containertest "k8s.io/kubernetes/pkg/kubelet/container/testing"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/kubelet/network"
nettest "k8s.io/kubernetes/pkg/kubelet/network/testing"
"k8s.io/kubernetes/pkg/kubelet/util/cache"
)
// newTestNetworkPlugin returns a mock plugin that implements network.NetworkPlugin
func newTestNetworkPlugin(t *testing.T) *nettest.MockNetworkPlugin {
ctrl := gomock.NewController(t)
return nettest.NewMockNetworkPlugin(ctrl)
}
func newTestDockerService() (*dockerService, *libdocker.FakeDockerClient, *clock.FakeClock) {
fakeClock := clock.NewFakeClock(time.Time{})
c := libdocker.NewFakeDockerClient().WithClock(fakeClock).WithVersion("1.11.2", "1.23")
pm := network.NewPluginManager(&network.NoopNetworkPlugin{})
return &dockerService{
client: c,
os: &containertest.FakeOS{},
network: pm,
checkpointHandler: NewTestPersistentCheckpointHandler(),
networkReady: make(map[string]bool),
}, c, fakeClock
}
func newTestDockerServiceWithVersionCache() (*dockerService, *libdocker.FakeDockerClient, *clock.FakeClock) {
ds, c, fakeClock := newTestDockerService()
ds.versionCache = cache.NewObjectCache(
func() (interface{}, error) {
return ds.getDockerVersion()
},
time.Hour*10,
)
return ds, c, fakeClock
}
// TestStatus tests the runtime status logic.
func TestStatus(t *testing.T) {
ds, fDocker, _ := newTestDockerService()
assertStatus := func(expected map[string]bool, status *runtimeapi.RuntimeStatus) {
conditions := status.GetConditions()
assert.Equal(t, len(expected), len(conditions))
for k, v := range expected {
for _, c := range conditions {
if k == c.Type {
assert.Equal(t, v, c.Status)
}
}
}
}
// Should report ready status if version returns no error.
status, err := ds.Status()
assert.NoError(t, err)
assertStatus(map[string]bool{
runtimeapi.RuntimeReady: true,
runtimeapi.NetworkReady: true,
}, status)
// Should not report ready status if version returns error.
fDocker.InjectError("version", errors.New("test error"))
status, err = ds.Status()
assert.NoError(t, err)
assertStatus(map[string]bool{
runtimeapi.RuntimeReady: false,
runtimeapi.NetworkReady: true,
}, status)
// Should not report ready status is network plugin returns error.
mockPlugin := newTestNetworkPlugin(t)
ds.network = network.NewPluginManager(mockPlugin)
defer mockPlugin.Finish()
mockPlugin.EXPECT().Status().Return(errors.New("network error"))
status, err = ds.Status()
assert.NoError(t, err)
assertStatus(map[string]bool{
runtimeapi.RuntimeReady: true,
runtimeapi.NetworkReady: false,
}, status)
}
func TestVersion(t *testing.T) {
ds, _, _ := newTestDockerService()
expectedVersion := &dockertypes.Version{Version: "1.11.2", APIVersion: "1.23.0"}
v, err := ds.getDockerVersion()
require.NoError(t, err)
assert.Equal(t, expectedVersion, v)
expectedAPIVersion := &semver.Version{Major: 1, Minor: 23, Patch: 0}
apiVersion, err := ds.getDockerAPIVersion()
require.NoError(t, err)
assert.Equal(t, expectedAPIVersion, apiVersion)
}
func TestAPIVersionWithCache(t *testing.T) {
ds, _, _ := newTestDockerServiceWithVersionCache()
expected := &semver.Version{Major: 1, Minor: 23, Patch: 0}
version, err := ds.getDockerAPIVersion()
require.NoError(t, err)
assert.Equal(t, expected, version)
}

View File

@ -0,0 +1,34 @@
// +build linux
/*
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 dockershim
import (
"fmt"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ContainerStats returns stats for a container stats request based on container id.
func (ds *dockerService) ContainerStats(string) (*runtimeapi.ContainerStats, error) {
return nil, fmt.Errorf("not implemented")
}
// ListContainerStats returns stats for a list container stats request based on a filter.
func (ds *dockerService) ListContainerStats(*runtimeapi.ContainerStatsFilter) ([]*runtimeapi.ContainerStats, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@ -0,0 +1,35 @@
// +build !linux,!windows
/*
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 dockershim
import (
"fmt"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ContainerStats returns stats for a container stats request based on container id.
func (ds *dockerService) ContainerStats(string) (*runtimeapi.ContainerStats, error) {
return nil, fmt.Errorf("not implemented")
}
// ListContainerStats returns stats for a list container stats request based on a filter.
func (ds *dockerService) ListContainerStats(*runtimeapi.ContainerStatsFilter) ([]*runtimeapi.ContainerStats, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@ -0,0 +1,105 @@
// +build windows
/*
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 dockershim
import (
"time"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// ContainerStats returns stats for a container stats request based on container id.
func (ds *dockerService) ContainerStats(containerID string) (*runtimeapi.ContainerStats, error) {
containerStats, err := ds.getContainerStats(containerID)
if err != nil {
return nil, err
}
return containerStats, nil
}
// ListContainerStats returns stats for a list container stats request based on a filter.
func (ds *dockerService) ListContainerStats(containerStatsFilter *runtimeapi.ContainerStatsFilter) ([]*runtimeapi.ContainerStats, error) {
filter := &runtimeapi.ContainerFilter{}
if containerStatsFilter != nil {
filter.Id = containerStatsFilter.Id
filter.PodSandboxId = containerStatsFilter.PodSandboxId
filter.LabelSelector = containerStatsFilter.LabelSelector
}
containers, err := ds.ListContainers(filter)
if err != nil {
return nil, err
}
var stats []*runtimeapi.ContainerStats
for _, container := range containers {
containerStats, err := ds.getContainerStats(container.Id)
if err != nil {
return nil, err
}
stats = append(stats, containerStats)
}
return stats, nil
}
func (ds *dockerService) getContainerStats(containerID string) (*runtimeapi.ContainerStats, error) {
statsJSON, err := ds.client.GetContainerStats(containerID)
if err != nil {
return nil, err
}
containerJSON, err := ds.client.InspectContainerWithSize(containerID)
if err != nil {
return nil, err
}
status, err := ds.ContainerStatus(containerID)
if err != nil {
return nil, err
}
dockerStats := statsJSON.Stats
timestamp := time.Now().UnixNano()
containerStats := &runtimeapi.ContainerStats{
Attributes: &runtimeapi.ContainerAttributes{
Id: containerID,
Metadata: status.Metadata,
Labels: status.Labels,
Annotations: status.Annotations,
},
Cpu: &runtimeapi.CpuUsage{
Timestamp: timestamp,
// have to multiply cpu usage by 100 since docker stats units is in 100's of nano seconds for Windows
// see https://github.com/moby/moby/blob/v1.13.1/api/types/stats.go#L22
UsageCoreNanoSeconds: &runtimeapi.UInt64Value{Value: dockerStats.CPUStats.CPUUsage.TotalUsage * 100},
},
Memory: &runtimeapi.MemoryUsage{
Timestamp: timestamp,
WorkingSetBytes: &runtimeapi.UInt64Value{Value: dockerStats.MemoryStats.PrivateWorkingSet},
},
WritableLayer: &runtimeapi.FilesystemUsage{
Timestamp: timestamp,
UsedBytes: &runtimeapi.UInt64Value{Value: uint64(*containerJSON.SizeRw)},
},
}
return containerStats, nil
}

View File

@ -0,0 +1,217 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"bytes"
"fmt"
"io"
"math"
"os/exec"
"strings"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
"k8s.io/client-go/tools/remotecommand"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/server/streaming"
"k8s.io/kubernetes/pkg/kubelet/util/ioutils"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
type streamingRuntime struct {
client libdocker.Interface
execHandler ExecHandler
}
var _ streaming.Runtime = &streamingRuntime{}
func (r *streamingRuntime) Exec(containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
return r.exec(containerID, cmd, in, out, err, tty, resize, 0)
}
// Internal version of Exec adds a timeout.
func (r *streamingRuntime) exec(containerID string, cmd []string, in io.Reader, out, errw io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
container, err := checkContainerStatus(r.client, containerID)
if err != nil {
return err
}
return r.execHandler.ExecInContainer(r.client, container, cmd, in, out, errw, tty, resize, timeout)
}
func (r *streamingRuntime) Attach(containerID string, in io.Reader, out, errw io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
_, err := checkContainerStatus(r.client, containerID)
if err != nil {
return err
}
return attachContainer(r.client, containerID, in, out, errw, tty, resize)
}
func (r *streamingRuntime) PortForward(podSandboxID string, port int32, stream io.ReadWriteCloser) error {
if port < 0 || port > math.MaxUint16 {
return fmt.Errorf("invalid port %d", port)
}
return portForward(r.client, podSandboxID, port, stream)
}
// ExecSync executes a command in the container, and returns the stdout output.
// If command exits with a non-zero exit code, an error is returned.
func (ds *dockerService) ExecSync(containerID string, cmd []string, timeout time.Duration) (stdout []byte, stderr []byte, err error) {
var stdoutBuffer, stderrBuffer bytes.Buffer
err = ds.streamingRuntime.exec(containerID, cmd,
nil, // in
ioutils.WriteCloserWrapper(&stdoutBuffer),
ioutils.WriteCloserWrapper(&stderrBuffer),
false, // tty
nil, // resize
timeout)
return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err
}
// Exec prepares a streaming endpoint to execute a command in the container, and returns the address.
func (ds *dockerService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
if ds.streamingServer == nil {
return nil, streaming.ErrorStreamingDisabled("exec")
}
_, err := checkContainerStatus(ds.client, req.ContainerId)
if err != nil {
return nil, err
}
return ds.streamingServer.GetExec(req)
}
// Attach prepares a streaming endpoint to attach to a running container, and returns the address.
func (ds *dockerService) Attach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) {
if ds.streamingServer == nil {
return nil, streaming.ErrorStreamingDisabled("attach")
}
_, err := checkContainerStatus(ds.client, req.ContainerId)
if err != nil {
return nil, err
}
return ds.streamingServer.GetAttach(req)
}
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address.
func (ds *dockerService) PortForward(req *runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error) {
if ds.streamingServer == nil {
return nil, streaming.ErrorStreamingDisabled("port forward")
}
_, err := checkContainerStatus(ds.client, req.PodSandboxId)
if err != nil {
return nil, err
}
// TODO(tallclair): Verify that ports are exposed.
return ds.streamingServer.GetPortForward(req)
}
func checkContainerStatus(client libdocker.Interface, containerID string) (*dockertypes.ContainerJSON, error) {
container, err := client.InspectContainer(containerID)
if err != nil {
return nil, err
}
if !container.State.Running {
return nil, fmt.Errorf("container not running (%s)", container.ID)
}
return container, nil
}
func attachContainer(client libdocker.Interface, containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
// Have to start this before the call to client.AttachToContainer because client.AttachToContainer is a blocking
// call :-( Otherwise, resize events don't get processed and the terminal never resizes.
kubecontainer.HandleResizing(resize, func(size remotecommand.TerminalSize) {
client.ResizeContainerTTY(containerID, uint(size.Height), uint(size.Width))
})
// TODO(random-liu): Do we really use the *Logs* field here?
opts := dockertypes.ContainerAttachOptions{
Stream: true,
Stdin: stdin != nil,
Stdout: stdout != nil,
Stderr: stderr != nil,
}
sopts := libdocker.StreamOptions{
InputStream: stdin,
OutputStream: stdout,
ErrorStream: stderr,
RawTerminal: tty,
}
return client.AttachToContainer(containerID, opts, sopts)
}
func portForward(client libdocker.Interface, podSandboxID string, port int32, stream io.ReadWriteCloser) error {
container, err := client.InspectContainer(podSandboxID)
if err != nil {
return err
}
if !container.State.Running {
return fmt.Errorf("container not running (%s)", container.ID)
}
containerPid := container.State.Pid
socatPath, lookupErr := exec.LookPath("socat")
if lookupErr != nil {
return fmt.Errorf("unable to do port forwarding: socat not found.")
}
args := []string{"-t", fmt.Sprintf("%d", containerPid), "-n", socatPath, "-", fmt.Sprintf("TCP4:localhost:%d", port)}
nsenterPath, lookupErr := exec.LookPath("nsenter")
if lookupErr != nil {
return fmt.Errorf("unable to do port forwarding: nsenter not found.")
}
commandString := fmt.Sprintf("%s %s", nsenterPath, strings.Join(args, " "))
glog.V(4).Infof("executing port forwarding command: %s", commandString)
command := exec.Command(nsenterPath, args...)
command.Stdout = stream
stderr := new(bytes.Buffer)
command.Stderr = stderr
// If we use Stdin, command.Run() won't return until the goroutine that's copying
// from stream finishes. Unfortunately, if you have a client like telnet connected
// via port forwarding, as long as the user's telnet client is connected to the user's
// local listener that port forwarding sets up, the telnet session never exits. This
// means that even if socat has finished running, command.Run() won't ever return
// (because the client still has the connection and stream open).
//
// The work around is to use StdinPipe(), as Wait() (called by Run()) closes the pipe
// when the command (socat) exits.
inPipe, err := command.StdinPipe()
if err != nil {
return fmt.Errorf("unable to do port forwarding: error creating stdin pipe: %v", err)
}
go func() {
io.Copy(inPipe, stream)
inPipe.Close()
}()
if err := command.Run(); err != nil {
return fmt.Errorf("%v: %s", err, stderr.String())
}
return nil
}

135
vendor/k8s.io/kubernetes/pkg/kubelet/dockershim/exec.go generated vendored Normal file
View File

@ -0,0 +1,135 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"io"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
"k8s.io/client-go/tools/remotecommand"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
)
// ExecHandler knows how to execute a command in a running Docker container.
type ExecHandler interface {
ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error
}
type dockerExitError struct {
Inspect *dockertypes.ContainerExecInspect
}
func (d *dockerExitError) String() string {
return d.Error()
}
func (d *dockerExitError) Error() string {
return fmt.Sprintf("Error executing in Docker Container: %d", d.Inspect.ExitCode)
}
func (d *dockerExitError) Exited() bool {
return !d.Inspect.Running
}
func (d *dockerExitError) ExitStatus() int {
return d.Inspect.ExitCode
}
// NativeExecHandler executes commands in Docker containers using Docker's exec API.
type NativeExecHandler struct{}
func (*NativeExecHandler) ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
createOpts := dockertypes.ExecConfig{
Cmd: cmd,
AttachStdin: stdin != nil,
AttachStdout: stdout != nil,
AttachStderr: stderr != nil,
Tty: tty,
}
execObj, err := client.CreateExec(container.ID, createOpts)
if err != nil {
return fmt.Errorf("failed to exec in container - Exec setup failed - %v", err)
}
// Have to start this before the call to client.StartExec because client.StartExec is a blocking
// call :-( Otherwise, resize events don't get processed and the terminal never resizes.
//
// We also have to delay attempting to send a terminal resize request to docker until after the
// exec has started; otherwise, the initial resize request will fail.
execStarted := make(chan struct{})
go func() {
select {
case <-execStarted:
// client.StartExec has started the exec, so we can start resizing
case <-done:
// ExecInContainer has returned, so short-circuit
return
}
kubecontainer.HandleResizing(resize, func(size remotecommand.TerminalSize) {
client.ResizeExecTTY(execObj.ID, uint(size.Height), uint(size.Width))
})
}()
startOpts := dockertypes.ExecStartCheck{Detach: false, Tty: tty}
streamOpts := libdocker.StreamOptions{
InputStream: stdin,
OutputStream: stdout,
ErrorStream: stderr,
RawTerminal: tty,
ExecStarted: execStarted,
}
err = client.StartExec(execObj.ID, startOpts, streamOpts)
if err != nil {
return err
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
count := 0
for {
inspect, err2 := client.InspectExec(execObj.ID)
if err2 != nil {
return err2
}
if !inspect.Running {
if inspect.ExitCode != 0 {
err = &dockerExitError{inspect}
}
break
}
count++
if count == 5 {
glog.Errorf("Exec session %s in container %s terminated but process still running!", execObj.ID, container.ID)
break
}
<-ticker.C
}
return err
}

View File

@ -0,0 +1,417 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerfilters "github.com/docker/docker/api/types/filters"
dockernat "github.com/docker/go-connections/nat"
"github.com/golang/glog"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/kubernetes/pkg/credentialprovider"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/util/parsers"
)
const (
annotationPrefix = "annotation."
// Docker changed the API for specifying options in v1.11
securityOptSeparatorChangeVersion = "1.23.0" // Corresponds to docker 1.11.x
securityOptSeparatorOld = ':'
securityOptSeparatorNew = '='
)
var (
conflictRE = regexp.MustCompile(`Conflict. (?:.)+ is already in use by container ([0-9a-z]+)`)
// this is hacky, but extremely common.
// if a container starts but the executable file is not found, runc gives a message that matches
startRE = regexp.MustCompile(`\\\\\\\"(.*)\\\\\\\": executable file not found`)
// Docker changes the security option separator from ':' to '=' in the 1.23
// API version.
optsSeparatorChangeVersion = semver.MustParse(securityOptSeparatorChangeVersion)
defaultSeccompOpt = []dockerOpt{{"seccomp", "unconfined", ""}}
)
// generateEnvList converts KeyValue list to a list of strings, in the form of
// '<key>=<value>', which can be understood by docker.
func generateEnvList(envs []*runtimeapi.KeyValue) (result []string) {
for _, env := range envs {
result = append(result, fmt.Sprintf("%s=%s", env.Key, env.Value))
}
return
}
// makeLabels converts annotations to labels and merge them with the given
// labels. This is necessary because docker does not support annotations;
// we *fake* annotations using labels. Note that docker labels are not
// updatable.
func makeLabels(labels, annotations map[string]string) map[string]string {
merged := make(map[string]string)
for k, v := range labels {
merged[k] = v
}
for k, v := range annotations {
// Assume there won't be conflict.
merged[fmt.Sprintf("%s%s", annotationPrefix, k)] = v
}
return merged
}
// extractLabels converts raw docker labels to the CRI labels and annotations.
// It also filters out internal labels used by this shim.
func extractLabels(input map[string]string) (map[string]string, map[string]string) {
labels := make(map[string]string)
annotations := make(map[string]string)
for k, v := range input {
// Check if the key is used internally by the shim.
internal := false
for _, internalKey := range internalLabelKeys {
if k == internalKey {
internal = true
break
}
}
if internal {
continue
}
// Delete the container name label for the sandbox. It is added in the shim,
// should not be exposed via CRI.
if k == types.KubernetesContainerNameLabel &&
input[containerTypeLabelKey] == containerTypeLabelSandbox {
continue
}
// Check if the label should be treated as an annotation.
if strings.HasPrefix(k, annotationPrefix) {
annotations[strings.TrimPrefix(k, annotationPrefix)] = v
continue
}
labels[k] = v
}
return labels, annotations
}
// generateMountBindings converts the mount list to a list of strings that
// can be understood by docker.
// '<HostPath>:<ContainerPath>[:options]', where 'options'
// is a comma-separated list of the following strings:
// 'ro', if the path is read only
// 'Z', if the volume requires SELinux relabeling
// propagation mode such as 'rslave'
func generateMountBindings(mounts []*runtimeapi.Mount) []string {
result := make([]string, 0, len(mounts))
for _, m := range mounts {
bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath)
var attrs []string
if m.Readonly {
attrs = append(attrs, "ro")
}
// Only request relabeling if the pod provides an SELinux context. If the pod
// does not provide an SELinux context relabeling will label the volume with
// the container's randomly allocated MCS label. This would restrict access
// to the volume to the container which mounts it first.
if m.SelinuxRelabel {
attrs = append(attrs, "Z")
}
switch m.Propagation {
case runtimeapi.MountPropagation_PROPAGATION_PRIVATE:
// noop, private is default
case runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL:
attrs = append(attrs, "rshared")
case runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER:
attrs = append(attrs, "rslave")
default:
glog.Warningf("unknown propagation mode for hostPath %q", m.HostPath)
// Falls back to "private"
}
if len(attrs) > 0 {
bind = fmt.Sprintf("%s:%s", bind, strings.Join(attrs, ","))
}
result = append(result, bind)
}
return result
}
func makePortsAndBindings(pm []*runtimeapi.PortMapping) (dockernat.PortSet, map[dockernat.Port][]dockernat.PortBinding) {
exposedPorts := dockernat.PortSet{}
portBindings := map[dockernat.Port][]dockernat.PortBinding{}
for _, port := range pm {
exteriorPort := port.HostPort
if exteriorPort == 0 {
// No need to do port binding when HostPort is not specified
continue
}
interiorPort := port.ContainerPort
// Some of this port stuff is under-documented voodoo.
// See http://stackoverflow.com/questions/20428302/binding-a-port-to-a-host-interface-using-the-rest-api
var protocol string
switch port.Protocol {
case runtimeapi.Protocol_UDP:
protocol = "/udp"
case runtimeapi.Protocol_TCP:
protocol = "/tcp"
default:
glog.Warningf("Unknown protocol %q: defaulting to TCP", port.Protocol)
protocol = "/tcp"
}
dockerPort := dockernat.Port(strconv.Itoa(int(interiorPort)) + protocol)
exposedPorts[dockerPort] = struct{}{}
hostBinding := dockernat.PortBinding{
HostPort: strconv.Itoa(int(exteriorPort)),
HostIP: port.HostIp,
}
// Allow multiple host ports bind to same docker port
if existedBindings, ok := portBindings[dockerPort]; ok {
// If a docker port already map to a host port, just append the host ports
portBindings[dockerPort] = append(existedBindings, hostBinding)
} else {
// Otherwise, it's fresh new port binding
portBindings[dockerPort] = []dockernat.PortBinding{
hostBinding,
}
}
}
return exposedPorts, portBindings
}
// getApparmorSecurityOpts gets apparmor options from container config.
func getApparmorSecurityOpts(sc *runtimeapi.LinuxContainerSecurityContext, separator rune) ([]string, error) {
if sc == nil || sc.ApparmorProfile == "" {
return nil, nil
}
appArmorOpts, err := getAppArmorOpts(sc.ApparmorProfile)
if err != nil {
return nil, err
}
fmtOpts := fmtDockerOpts(appArmorOpts, separator)
return fmtOpts, nil
}
// dockerFilter wraps around dockerfilters.Args and provides methods to modify
// the filter easily.
type dockerFilter struct {
args *dockerfilters.Args
}
func newDockerFilter(args *dockerfilters.Args) *dockerFilter {
return &dockerFilter{args: args}
}
func (f *dockerFilter) Add(key, value string) {
f.args.Add(key, value)
}
func (f *dockerFilter) AddLabel(key, value string) {
f.Add("label", fmt.Sprintf("%s=%s", key, value))
}
// parseUserFromImageUser splits the user out of an user:group string.
func parseUserFromImageUser(id string) string {
if id == "" {
return id
}
// split instances where the id may contain user:group
if strings.Contains(id, ":") {
return strings.Split(id, ":")[0]
}
// no group, just return the id
return id
}
// getUserFromImageUser gets uid or user name of the image user.
// If user is numeric, it will be treated as uid; or else, it is treated as user name.
func getUserFromImageUser(imageUser string) (*int64, string) {
user := parseUserFromImageUser(imageUser)
// return both nil if user is not specified in the image.
if user == "" {
return nil, ""
}
// user could be either uid or user name. Try to interpret as numeric uid.
uid, err := strconv.ParseInt(user, 10, 64)
if err != nil {
// If user is non numeric, assume it's user name.
return nil, user
}
// If user is a numeric uid.
return &uid, ""
}
// See #33189. If the previous attempt to create a sandbox container name FOO
// failed due to "device or resource busy", it is possible that docker did
// not clean up properly and has inconsistent internal state. Docker would
// not report the existence of FOO, but would complain if user wants to
// create a new container named FOO. To work around this, we parse the error
// message to identify failure caused by naming conflict, and try to remove
// the old container FOO.
// See #40443. Sometimes even removal may fail with "no such container" error.
// In that case we have to create the container with a randomized name.
// TODO(random-liu): Remove this work around after docker 1.11 is deprecated.
// TODO(#33189): Monitor the tests to see if the fix is sufficient.
func recoverFromCreationConflictIfNeeded(client libdocker.Interface, createConfig dockertypes.ContainerCreateConfig, err error) (*dockercontainer.ContainerCreateCreatedBody, error) {
matches := conflictRE.FindStringSubmatch(err.Error())
if len(matches) != 2 {
return nil, err
}
id := matches[1]
glog.Warningf("Unable to create pod sandbox due to conflict. Attempting to remove sandbox %q", id)
if rmErr := client.RemoveContainer(id, dockertypes.ContainerRemoveOptions{RemoveVolumes: true}); rmErr == nil {
glog.V(2).Infof("Successfully removed conflicting container %q", id)
return nil, err
} else {
glog.Errorf("Failed to remove the conflicting container %q: %v", id, rmErr)
// Return if the error is not container not found error.
if !libdocker.IsContainerNotFoundError(rmErr) {
return nil, err
}
}
// randomize the name to avoid conflict.
createConfig.Name = randomizeName(createConfig.Name)
glog.V(2).Infof("Create the container with randomized name %s", createConfig.Name)
return client.CreateContainer(createConfig)
}
// transformStartContainerError does regex parsing on returned error
// for where container runtimes are giving less than ideal error messages.
func transformStartContainerError(err error) error {
if err == nil {
return nil
}
matches := startRE.FindStringSubmatch(err.Error())
if len(matches) > 0 {
return fmt.Errorf("executable not found in $PATH")
}
return err
}
// getSecurityOptSeparator returns the security option separator based on the
// docker API version.
// TODO: Remove this function along with the relevant code when we no longer
// need to support docker 1.10.
func getSecurityOptSeparator(v *semver.Version) rune {
switch v.Compare(optsSeparatorChangeVersion) {
case -1:
// Current version is less than the API change version; use the old
// separator.
return securityOptSeparatorOld
default:
return securityOptSeparatorNew
}
}
// ensureSandboxImageExists pulls the sandbox image when it's not present.
func ensureSandboxImageExists(client libdocker.Interface, image string) error {
_, err := client.InspectImageByRef(image)
if err == nil {
return nil
}
if !libdocker.IsImageNotFoundError(err) {
return fmt.Errorf("failed to inspect sandbox image %q: %v", image, err)
}
repoToPull, _, _, err := parsers.ParseImageName(image)
if err != nil {
return err
}
keyring := credentialprovider.NewDockerKeyring()
creds, withCredentials := keyring.Lookup(repoToPull)
if !withCredentials {
glog.V(3).Infof("Pulling image %q without credentials", image)
err := client.PullImage(image, dockertypes.AuthConfig{}, dockertypes.ImagePullOptions{})
if err != nil {
return fmt.Errorf("failed pulling image %q: %v", image, err)
}
return nil
}
var pullErrs []error
for _, currentCreds := range creds {
authConfig := credentialprovider.LazyProvide(currentCreds)
err := client.PullImage(image, authConfig, dockertypes.ImagePullOptions{})
// If there was no error, return success
if err == nil {
return nil
}
pullErrs = append(pullErrs, err)
}
return utilerrors.NewAggregate(pullErrs)
}
func getAppArmorOpts(profile string) ([]dockerOpt, error) {
if profile == "" || profile == apparmor.ProfileRuntimeDefault {
// The docker applies the default profile by default.
return nil, nil
}
// Return unconfined profile explicitly
if profile == apparmor.ProfileNameUnconfined {
return []dockerOpt{{"apparmor", apparmor.ProfileNameUnconfined, ""}}, nil
}
// Assume validation has already happened.
profileName := strings.TrimPrefix(profile, apparmor.ProfileNamePrefix)
return []dockerOpt{{"apparmor", profileName, ""}}, nil
}
// fmtDockerOpts formats the docker security options using the given separator.
func fmtDockerOpts(opts []dockerOpt, sep rune) []string {
fmtOpts := make([]string, len(opts))
for i, opt := range opts {
fmtOpts[i] = fmt.Sprintf("%s%c%s", opt.key, sep, opt.value)
}
return fmtOpts
}
type dockerOpt struct {
// The key-value pair passed to docker.
key, value string
// The alternative value to use in log/event messages.
msg string
}
// Expose key/value from dockerOpt.
func (d dockerOpt) GetKV() (string, string) {
return d.key, d.value
}

View File

@ -0,0 +1,147 @@
// +build linux
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"bytes"
"crypto/md5"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func DefaultMemorySwap() int64 {
return 0
}
func (ds *dockerService) getSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
// Apply seccomp options.
seccompSecurityOpts, err := getSeccompSecurityOpts(seccompProfile, separator)
if err != nil {
return nil, fmt.Errorf("failed to generate seccomp security options for container: %v", err)
}
return seccompSecurityOpts, nil
}
func getSeccompDockerOpts(seccompProfile string) ([]dockerOpt, error) {
if seccompProfile == "" || seccompProfile == "unconfined" {
// return early the default
return defaultSeccompOpt, nil
}
if seccompProfile == "docker/default" {
// return nil so docker will load the default seccomp profile
return nil, nil
}
if !strings.HasPrefix(seccompProfile, "localhost/") {
return nil, fmt.Errorf("unknown seccomp profile option: %s", seccompProfile)
}
// get the full path of seccomp profile when prefixed with 'localhost/'.
fname := strings.TrimPrefix(seccompProfile, "localhost/")
if !filepath.IsAbs(fname) {
return nil, fmt.Errorf("seccomp profile path must be absolute, but got relative path %q", fname)
}
file, err := ioutil.ReadFile(filepath.FromSlash(fname))
if err != nil {
return nil, fmt.Errorf("cannot load seccomp profile %q: %v", fname, err)
}
b := bytes.NewBuffer(nil)
if err := json.Compact(b, file); err != nil {
return nil, err
}
// Rather than the full profile, just put the filename & md5sum in the event log.
msg := fmt.Sprintf("%s(md5:%x)", fname, md5.Sum(file))
return []dockerOpt{{"seccomp", b.String(), msg}}, nil
}
// getSeccompSecurityOpts gets container seccomp options from container seccomp profile.
// It is an experimental feature and may be promoted to official runtime api in the future.
func getSeccompSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
seccompOpts, err := getSeccompDockerOpts(seccompProfile)
if err != nil {
return nil, err
}
return fmtDockerOpts(seccompOpts, separator), nil
}
func (ds *dockerService) updateCreateConfig(
createConfig *dockertypes.ContainerCreateConfig,
config *runtimeapi.ContainerConfig,
sandboxConfig *runtimeapi.PodSandboxConfig,
podSandboxID string, securityOptSep rune, apiVersion *semver.Version) error {
// Apply Linux-specific options if applicable.
if lc := config.GetLinux(); lc != nil {
// TODO: Check if the units are correct.
// TODO: Can we assume the defaults are sane?
rOpts := lc.GetResources()
if rOpts != nil {
createConfig.HostConfig.Resources = dockercontainer.Resources{
Memory: rOpts.MemoryLimitInBytes,
MemorySwap: DefaultMemorySwap(),
CPUShares: rOpts.CpuShares,
CPUQuota: rOpts.CpuQuota,
CPUPeriod: rOpts.CpuPeriod,
}
createConfig.HostConfig.OomScoreAdj = int(rOpts.OomScoreAdj)
}
// Note: ShmSize is handled in kube_docker_client.go
// Apply security context.
if err := applyContainerSecurityContext(lc, podSandboxID, createConfig.Config, createConfig.HostConfig, securityOptSep); err != nil {
return fmt.Errorf("failed to apply container security context for container %q: %v", config.Metadata.Name, err)
}
modifyPIDNamespaceOverrides(ds.disableSharedPID, apiVersion, createConfig.HostConfig)
}
// Apply cgroupsParent derived from the sandbox config.
if lc := sandboxConfig.GetLinux(); lc != nil {
// Apply Cgroup options.
cgroupParent, err := ds.GenerateExpectedCgroupParent(lc.CgroupParent)
if err != nil {
return fmt.Errorf("failed to generate cgroup parent in expected syntax for container %q: %v", config.Metadata.Name, err)
}
createConfig.HostConfig.CgroupParent = cgroupParent
}
return nil
}
func (ds *dockerService) determinePodIPBySandboxID(uid string) string {
return ""
}
func getNetworkNamespace(c *dockertypes.ContainerJSON) (string, error) {
if c.State.Pid == 0 {
// Docker reports pid 0 for an exited container.
return "", fmt.Errorf("cannot find network namespace for the terminated container %q", c.ID)
}
return fmt.Sprintf(dockerNetNSFmt, c.State.Pid), nil
}

View File

@ -0,0 +1,103 @@
// +build linux
/*
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 dockershim
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSeccompSecurityOpts(t *testing.T) {
tests := []struct {
msg string
seccompProfile string
expectedOpts []string
}{{
msg: "No security annotations",
seccompProfile: "",
expectedOpts: []string{"seccomp=unconfined"},
}, {
msg: "Seccomp unconfined",
seccompProfile: "unconfined",
expectedOpts: []string{"seccomp=unconfined"},
}, {
msg: "Seccomp default",
seccompProfile: "docker/default",
expectedOpts: nil,
}}
for i, test := range tests {
opts, err := getSeccompSecurityOpts(test.seccompProfile, '=')
assert.NoError(t, err, "TestCase[%d]: %s", i, test.msg)
assert.Len(t, opts, len(test.expectedOpts), "TestCase[%d]: %s", i, test.msg)
for _, opt := range test.expectedOpts {
assert.Contains(t, opts, opt, "TestCase[%d]: %s", i, test.msg)
}
}
}
func TestLoadSeccompLocalhostProfiles(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "seccomp-local-profile-test")
require.NoError(t, err)
defer os.RemoveAll(tmpdir)
testProfile := `{"foo": "bar"}`
err = ioutil.WriteFile(filepath.Join(tmpdir, "test"), []byte(testProfile), 0644)
require.NoError(t, err)
tests := []struct {
msg string
seccompProfile string
expectedOpts []string
expectErr bool
}{{
msg: "Seccomp localhost/test profile should return correct seccomp profiles",
seccompProfile: "localhost/" + filepath.Join(tmpdir, "test"),
expectedOpts: []string{`seccomp={"foo":"bar"}`},
expectErr: false,
}, {
msg: "Non-existent profile should return error",
seccompProfile: "localhost/" + filepath.Join(tmpdir, "fixtures/non-existent"),
expectedOpts: nil,
expectErr: true,
}, {
msg: "Relative profile path should return error",
seccompProfile: "localhost/fixtures/test",
expectedOpts: nil,
expectErr: true,
}}
for i, test := range tests {
opts, err := getSeccompSecurityOpts(test.seccompProfile, '=')
if test.expectErr {
assert.Error(t, err, fmt.Sprintf("TestCase[%d]: %s", i, test.msg))
continue
}
assert.NoError(t, err, "TestCase[%d]: %s", i, test.msg)
assert.Len(t, opts, len(test.expectedOpts), "TestCase[%d]: %s", i, test.msg)
for _, opt := range test.expectedOpts {
assert.Contains(t, opts, opt, "TestCase[%d]: %s", i, test.msg)
}
}
}

View File

@ -0,0 +1,371 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
dockernat "github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker"
"k8s.io/kubernetes/pkg/security/apparmor"
)
func TestLabelsAndAnnotationsRoundTrip(t *testing.T) {
expectedLabels := map[string]string{"foo.123.abc": "baz", "bar.456.xyz": "qwe"}
expectedAnnotations := map[string]string{"uio.ert": "dfs", "jkl": "asd"}
// Merge labels and annotations into docker labels.
dockerLabels := makeLabels(expectedLabels, expectedAnnotations)
// Extract labels and annotations from docker labels.
actualLabels, actualAnnotations := extractLabels(dockerLabels)
assert.Equal(t, expectedLabels, actualLabels)
assert.Equal(t, expectedAnnotations, actualAnnotations)
}
// TestGetApparmorSecurityOpts tests the logic of generating container apparmor options from sandbox annotations.
func TestGetApparmorSecurityOpts(t *testing.T) {
makeConfig := func(profile string) *runtimeapi.LinuxContainerSecurityContext {
return &runtimeapi.LinuxContainerSecurityContext{
ApparmorProfile: profile,
}
}
tests := []struct {
msg string
config *runtimeapi.LinuxContainerSecurityContext
expectedOpts []string
}{{
msg: "No AppArmor options",
config: makeConfig(""),
expectedOpts: nil,
}, {
msg: "AppArmor runtime/default",
config: makeConfig("runtime/default"),
expectedOpts: []string{},
}, {
msg: "AppArmor local profile",
config: makeConfig(apparmor.ProfileNamePrefix + "foo"),
expectedOpts: []string{"apparmor=foo"},
}}
for i, test := range tests {
opts, err := getApparmorSecurityOpts(test.config, '=')
assert.NoError(t, err, "TestCase[%d]: %s", i, test.msg)
assert.Len(t, opts, len(test.expectedOpts), "TestCase[%d]: %s", i, test.msg)
for _, opt := range test.expectedOpts {
assert.Contains(t, opts, opt, "TestCase[%d]: %s", i, test.msg)
}
}
}
// TestGetUserFromImageUser tests the logic of getting image uid or user name of image user.
func TestGetUserFromImageUser(t *testing.T) {
newI64 := func(i int64) *int64 { return &i }
for c, test := range map[string]struct {
user string
uid *int64
name string
}{
"no gid": {
user: "0",
uid: newI64(0),
},
"uid/gid": {
user: "0:1",
uid: newI64(0),
},
"empty user": {
user: "",
},
"multiple spearators": {
user: "1:2:3",
uid: newI64(1),
},
"root username": {
user: "root:root",
name: "root",
},
"username": {
user: "test:test",
name: "test",
},
} {
t.Logf("TestCase - %q", c)
actualUID, actualName := getUserFromImageUser(test.user)
assert.Equal(t, test.uid, actualUID)
assert.Equal(t, test.name, actualName)
}
}
func TestParsingCreationConflictError(t *testing.T) {
// Expected error message from docker.
msg := "Conflict. The name \"/k8s_POD_pfpod_e2e-tests-port-forwarding-dlxt2_81a3469e-99e1-11e6-89f2-42010af00002_0\" is already in use by container 24666ab8c814d16f986449e504ea0159468ddf8da01897144a770f66dce0e14e. You have to remove (or rename) that container to be able to reuse that name."
matches := conflictRE.FindStringSubmatch(msg)
require.Len(t, matches, 2)
require.Equal(t, matches[1], "24666ab8c814d16f986449e504ea0159468ddf8da01897144a770f66dce0e14e")
}
func TestGetSecurityOptSeparator(t *testing.T) {
for c, test := range map[string]struct {
desc string
version *semver.Version
expected rune
}{
"older docker version": {
version: &semver.Version{Major: 1, Minor: 22, Patch: 0},
expected: ':',
},
"changed docker version": {
version: &semver.Version{Major: 1, Minor: 23, Patch: 0},
expected: '=',
},
"newer docker version": {
version: &semver.Version{Major: 1, Minor: 24, Patch: 0},
expected: '=',
},
} {
actual := getSecurityOptSeparator(test.version)
assert.Equal(t, test.expected, actual, c)
}
}
// writeDockerConfig will write a config file into a temporary dir, and return that dir.
// Caller is responsible for deleting the dir and its contents.
func writeDockerConfig(cfg string) (string, error) {
tmpdir, err := ioutil.TempDir("", "dockershim=helpers_test.go=")
if err != nil {
return "", err
}
dir := filepath.Join(tmpdir, ".docker")
if err := os.Mkdir(dir, 0755); err != nil {
return "", err
}
return tmpdir, ioutil.WriteFile(filepath.Join(dir, "config.json"), []byte(cfg), 0644)
}
func TestEnsureSandboxImageExists(t *testing.T) {
sandboxImage := "gcr.io/test/image"
authConfig := dockertypes.AuthConfig{Username: "user", Password: "pass"}
for desc, test := range map[string]struct {
injectImage bool
imgNeedsAuth bool
injectErr error
calls []string
err bool
configJSON string
}{
"should not pull image when it already exists": {
injectImage: true,
injectErr: nil,
calls: []string{"inspect_image"},
},
"should pull image when it doesn't exist": {
injectImage: false,
injectErr: libdocker.ImageNotFoundError{ID: "image_id"},
calls: []string{"inspect_image", "pull"},
},
"should return error when inspect image fails": {
injectImage: false,
injectErr: fmt.Errorf("arbitrary error"),
calls: []string{"inspect_image"},
err: true,
},
"should return error when image pull needs private auth, but none provided": {
injectImage: true,
imgNeedsAuth: true,
injectErr: libdocker.ImageNotFoundError{ID: "image_id"},
calls: []string{"inspect_image", "pull"},
err: true,
},
} {
t.Logf("TestCase: %q", desc)
_, fakeDocker, _ := newTestDockerService()
if test.injectImage {
images := []dockertypes.ImageSummary{{ID: sandboxImage}}
fakeDocker.InjectImages(images)
if test.imgNeedsAuth {
fakeDocker.MakeImagesPrivate(images, authConfig)
}
}
fakeDocker.InjectError("inspect_image", test.injectErr)
err := ensureSandboxImageExists(fakeDocker, sandboxImage)
assert.NoError(t, fakeDocker.AssertCalls(test.calls))
assert.Equal(t, test.err, err != nil)
}
}
func TestMakePortsAndBindings(t *testing.T) {
for desc, test := range map[string]struct {
pm []*runtimeapi.PortMapping
exposedPorts dockernat.PortSet
portmappings map[dockernat.Port][]dockernat.PortBinding
}{
"no port mapping": {
pm: nil,
exposedPorts: map[dockernat.Port]struct{}{},
portmappings: map[dockernat.Port][]dockernat.PortBinding{},
},
"tcp port mapping": {
pm: []*runtimeapi.PortMapping{
{
Protocol: runtimeapi.Protocol_TCP,
ContainerPort: 80,
HostPort: 80,
},
},
exposedPorts: map[dockernat.Port]struct{}{
"80/tcp": {},
},
portmappings: map[dockernat.Port][]dockernat.PortBinding{
"80/tcp": {
{
HostPort: "80",
},
},
},
},
"udp port mapping": {
pm: []*runtimeapi.PortMapping{
{
Protocol: runtimeapi.Protocol_UDP,
ContainerPort: 80,
HostPort: 80,
},
},
exposedPorts: map[dockernat.Port]struct{}{
"80/udp": {},
},
portmappings: map[dockernat.Port][]dockernat.PortBinding{
"80/udp": {
{
HostPort: "80",
},
},
},
},
"multipe port mappings": {
pm: []*runtimeapi.PortMapping{
{
Protocol: runtimeapi.Protocol_TCP,
ContainerPort: 80,
HostPort: 80,
},
{
Protocol: runtimeapi.Protocol_TCP,
ContainerPort: 80,
HostPort: 81,
},
},
exposedPorts: map[dockernat.Port]struct{}{
"80/tcp": {},
},
portmappings: map[dockernat.Port][]dockernat.PortBinding{
"80/tcp": {
{
HostPort: "80",
},
{
HostPort: "81",
},
},
},
},
} {
t.Logf("TestCase: %s", desc)
actualExposedPorts, actualPortMappings := makePortsAndBindings(test.pm)
assert.Equal(t, test.exposedPorts, actualExposedPorts)
assert.Equal(t, test.portmappings, actualPortMappings)
}
}
func TestGenerateMountBindings(t *testing.T) {
mounts := []*runtimeapi.Mount{
// everything default
{
HostPath: "/mnt/1",
ContainerPath: "/var/lib/mysql/1",
},
// readOnly
{
HostPath: "/mnt/2",
ContainerPath: "/var/lib/mysql/2",
Readonly: true,
},
// SELinux
{
HostPath: "/mnt/3",
ContainerPath: "/var/lib/mysql/3",
SelinuxRelabel: true,
},
// Propagation private
{
HostPath: "/mnt/4",
ContainerPath: "/var/lib/mysql/4",
Propagation: runtimeapi.MountPropagation_PROPAGATION_PRIVATE,
},
// Propagation rslave
{
HostPath: "/mnt/5",
ContainerPath: "/var/lib/mysql/5",
Propagation: runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER,
},
// Propagation rshared
{
HostPath: "/mnt/6",
ContainerPath: "/var/lib/mysql/6",
Propagation: runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL,
},
// Propagation unknown (falls back to private)
{
HostPath: "/mnt/7",
ContainerPath: "/var/lib/mysql/7",
Propagation: runtimeapi.MountPropagation(42),
},
// Everything
{
HostPath: "/mnt/8",
ContainerPath: "/var/lib/mysql/8",
Readonly: true,
SelinuxRelabel: true,
Propagation: runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL,
},
}
expectedResult := []string{
"/mnt/1:/var/lib/mysql/1",
"/mnt/2:/var/lib/mysql/2:ro",
"/mnt/3:/var/lib/mysql/3:Z",
"/mnt/4:/var/lib/mysql/4",
"/mnt/5:/var/lib/mysql/5:rslave",
"/mnt/6:/var/lib/mysql/6:rshared",
"/mnt/7:/var/lib/mysql/7",
"/mnt/8:/var/lib/mysql/8:ro,Z,rshared",
}
result := generateMountBindings(mounts)
assert.Equal(t, result, expectedResult)
}

View File

@ -0,0 +1,55 @@
// +build !linux,!windows
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func DefaultMemorySwap() int64 {
return -1
}
func (ds *dockerService) getSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
glog.Warningf("getSecurityOpts is unsupported in this build")
return nil, nil
}
func (ds *dockerService) updateCreateConfig(
createConfig *dockertypes.ContainerCreateConfig,
config *runtimeapi.ContainerConfig,
sandboxConfig *runtimeapi.PodSandboxConfig,
podSandboxID string, securityOptSep rune, apiVersion *semver.Version) error {
glog.Warningf("updateCreateConfig is unsupported in this build")
return nil
}
func (ds *dockerService) determinePodIPBySandboxID(uid string) string {
glog.Warningf("determinePodIPBySandboxID is unsupported in this build")
return ""
}
func getNetworkNamespace(c *dockertypes.ContainerJSON) (string, error) {
return "", fmt.Errorf("unsupported platform")
}

View File

@ -0,0 +1,129 @@
// +build windows
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"os"
"github.com/blang/semver"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerfilters "github.com/docker/docker/api/types/filters"
"github.com/golang/glog"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func DefaultMemorySwap() int64 {
return 0
}
func (ds *dockerService) getSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
if seccompProfile != "" {
glog.Warningf("seccomp annotations are not supported on windows")
}
return nil, nil
}
func (ds *dockerService) updateCreateConfig(
createConfig *dockertypes.ContainerCreateConfig,
config *runtimeapi.ContainerConfig,
sandboxConfig *runtimeapi.PodSandboxConfig,
podSandboxID string, securityOptSep rune, apiVersion *semver.Version) error {
if networkMode := os.Getenv("CONTAINER_NETWORK"); networkMode != "" {
createConfig.HostConfig.NetworkMode = dockercontainer.NetworkMode(networkMode)
} else {
// Todo: Refactor this call in future for calling methods directly in security_context.go
modifyHostNetworkOptionForContainer(false, podSandboxID, createConfig.HostConfig)
}
return nil
}
func (ds *dockerService) determinePodIPBySandboxID(sandboxID string) string {
opts := dockertypes.ContainerListOptions{
All: true,
Filters: dockerfilters.NewArgs(),
}
f := newDockerFilter(&opts.Filters)
f.AddLabel(containerTypeLabelKey, containerTypeLabelContainer)
f.AddLabel(sandboxIDLabelKey, sandboxID)
containers, err := ds.client.ListContainers(opts)
if err != nil {
return ""
}
for _, c := range containers {
r, err := ds.client.InspectContainer(c.ID)
if err != nil {
continue
}
// Versions and feature support
// ============================
// Windows version == Windows Server, Version 1709,, Supports both sandbox and non-sandbox case
// Windows version == Windows Server 2016 Support only non-sandbox case
// Windows version < Windows Server 2016 is Not Supported
// Sandbox support in Windows mandates CNI Plugin.
// Presense of CONTAINER_NETWORK flag is considered as non-Sandbox cases here
// Todo: Add a kernel version check for more validation
if networkMode := os.Getenv("CONTAINER_NETWORK"); networkMode == "" {
// Do not return any IP, so that we would continue and get the IP of the Sandbox
ds.getIP(sandboxID, r)
} else {
// On Windows, every container that is created in a Sandbox, needs to invoke CNI plugin again for adding the Network,
// with the shared container name as NetNS info,
// This is passed down to the platform to replicate some necessary information to the new container
//
// This place is chosen as a hack for now, since getContainerIP would end up calling CNI's addToNetwork
// That is why addToNetwork is required to be idempotent
// Instead of relying on this call, an explicit call to addToNetwork should be
// done immediately after ContainerCreation, in case of Windows only. TBD Issue # to handle this
if containerIP := getContainerIP(r); containerIP != "" {
return containerIP
}
}
}
return ""
}
func getNetworkNamespace(c *dockertypes.ContainerJSON) (string, error) {
// Currently in windows there is no identifier exposed for network namespace
// Like docker, the referenced container id is used to figure out the network namespace id internally by the platform
// so returning the docker networkMode (which holds container:<ref containerid> for network namespace here
return string(c.HostConfig.NetworkMode), nil
}
func getContainerIP(container *dockertypes.ContainerJSON) string {
if container.NetworkSettings != nil {
for _, network := range container.NetworkSettings.Networks {
if network.IPAddress != "" {
return network.IPAddress
}
}
}
return ""
}

View File

@ -0,0 +1,61 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = [
"helpers_test.go",
"kube_docker_client_test.go",
],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker",
library = ":go_default_library",
deps = [
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"fake_client.go",
"helpers.go",
"instrumented_client.go",
"kube_docker_client.go",
],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker",
deps = [
"//pkg/kubelet/dockershim/metrics:go_default_library",
"//vendor/github.com/docker/distribution/reference:go_default_library",
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/docker/docker/api/types/container:go_default_library",
"//vendor/github.com/docker/docker/api/types/image:go_default_library",
"//vendor/github.com/docker/docker/client:go_default_library",
"//vendor/github.com/docker/docker/pkg/jsonmessage:go_default_library",
"//vendor/github.com/docker/docker/pkg/stdcopy:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/opencontainers/go-digest:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,137 @@
/*
Copyright 2014 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 libdocker
import (
"strings"
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerimagetypes "github.com/docker/docker/api/types/image"
dockerapi "github.com/docker/docker/client"
"github.com/golang/glog"
)
const (
// https://docs.docker.com/engine/reference/api/docker_remote_api/
// docker version should be at least 1.10.x
MinimumDockerAPIVersion = "1.22.0"
// Status of a container returned by ListContainers.
StatusRunningPrefix = "Up"
StatusCreatedPrefix = "Created"
StatusExitedPrefix = "Exited"
// This is only used by GetKubeletDockerContainers(), and should be removed
// along with the function.
containerNamePrefix = "k8s"
// Fake docker endpoint
FakeDockerEndpoint = "fake://"
)
// Interface is an abstract interface for testability. It abstracts the interface of docker client.
type Interface interface {
ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error)
InspectContainer(id string) (*dockertypes.ContainerJSON, error)
InspectContainerWithSize(id string) (*dockertypes.ContainerJSON, error)
CreateContainer(dockertypes.ContainerCreateConfig) (*dockercontainer.ContainerCreateCreatedBody, error)
StartContainer(id string) error
StopContainer(id string, timeout time.Duration) error
UpdateContainerResources(id string, updateConfig dockercontainer.UpdateConfig) error
RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error
InspectImageByRef(imageRef string) (*dockertypes.ImageInspect, error)
InspectImageByID(imageID string) (*dockertypes.ImageInspect, error)
ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.ImageSummary, error)
PullImage(image string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error
RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error)
ImageHistory(id string) ([]dockerimagetypes.HistoryResponseItem, error)
Logs(string, dockertypes.ContainerLogsOptions, StreamOptions) error
Version() (*dockertypes.Version, error)
Info() (*dockertypes.Info, error)
CreateExec(string, dockertypes.ExecConfig) (*dockertypes.IDResponse, error)
StartExec(string, dockertypes.ExecStartCheck, StreamOptions) error
InspectExec(id string) (*dockertypes.ContainerExecInspect, error)
AttachToContainer(string, dockertypes.ContainerAttachOptions, StreamOptions) error
ResizeContainerTTY(id string, height, width uint) error
ResizeExecTTY(id string, height, width uint) error
GetContainerStats(id string) (*dockertypes.StatsJSON, error)
}
// Get a *dockerapi.Client, either using the endpoint passed in, or using
// DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT path per their spec
func getDockerClient(dockerEndpoint string) (*dockerapi.Client, error) {
if len(dockerEndpoint) > 0 {
glog.Infof("Connecting to docker on %s", dockerEndpoint)
return dockerapi.NewClient(dockerEndpoint, "", nil, nil)
}
return dockerapi.NewEnvClient()
}
// ConnectToDockerOrDie creates docker client connecting to docker daemon.
// If the endpoint passed in is "fake://", a fake docker client
// will be returned. The program exits if error occurs. The requestTimeout
// is the timeout for docker requests. If timeout is exceeded, the request
// will be cancelled and throw out an error. If requestTimeout is 0, a default
// value will be applied.
func ConnectToDockerOrDie(dockerEndpoint string, requestTimeout, imagePullProgressDeadline time.Duration,
withTraceDisabled bool, enableSleep bool) Interface {
if dockerEndpoint == FakeDockerEndpoint {
fakeClient := NewFakeDockerClient()
if withTraceDisabled {
fakeClient = fakeClient.WithTraceDisabled()
}
if enableSleep {
fakeClient.EnableSleep = true
}
return fakeClient
}
client, err := getDockerClient(dockerEndpoint)
if err != nil {
glog.Fatalf("Couldn't connect to docker: %v", err)
}
glog.Infof("Start docker client with request timeout=%v", requestTimeout)
return newKubeDockerClient(client, requestTimeout, imagePullProgressDeadline)
}
// GetKubeletDockerContainers lists all container or just the running ones.
// Returns a list of docker containers that we manage
// TODO: This function should be deleted after migrating
// test/e2e_node/garbage_collector_test.go off of it.
func GetKubeletDockerContainers(client Interface, allContainers bool) ([]*dockertypes.Container, error) {
result := []*dockertypes.Container{}
containers, err := client.ListContainers(dockertypes.ContainerListOptions{All: allContainers})
if err != nil {
return nil, err
}
for i := range containers {
container := &containers[i]
if len(container.Names) == 0 {
continue
}
// Skip containers that we didn't create to allow users to manually
// spin up their own containers if they want.
if !strings.HasPrefix(container.Names[0], "/"+containerNamePrefix+"_") {
glog.V(5).Infof("Docker Container: %s is not managed by kubelet.", container.Names[0])
continue
}
result = append(result, container)
}
return result, nil
}

View File

@ -0,0 +1,911 @@
/*
Copyright 2014 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 libdocker
import (
"encoding/json"
"fmt"
"hash/fnv"
"math/rand"
"os"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerimagetypes "github.com/docker/docker/api/types/image"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/clock"
)
type calledDetail struct {
name string
arguments []interface{}
}
// NewCalledDetail create a new call detail item.
func NewCalledDetail(name string, arguments []interface{}) calledDetail {
return calledDetail{name: name, arguments: arguments}
}
// FakeDockerClient is a simple fake docker client, so that kubelet can be run for testing without requiring a real docker setup.
type FakeDockerClient struct {
sync.Mutex
Clock clock.Clock
RunningContainerList []dockertypes.Container
ExitedContainerList []dockertypes.Container
ContainerMap map[string]*dockertypes.ContainerJSON
ImageInspects map[string]*dockertypes.ImageInspect
Images []dockertypes.ImageSummary
ImageIDsNeedingAuth map[string]dockertypes.AuthConfig
Errors map[string]error
called []calledDetail
pulled []string
EnableTrace bool
// Created, Started, Stopped and Removed all contain container docker ID
Created []string
Started []string
Stopped []string
Removed []string
// Images pulled by ref (name or ID).
ImagesPulled []string
VersionInfo dockertypes.Version
Information dockertypes.Info
ExecInspect *dockertypes.ContainerExecInspect
execCmd []string
EnableSleep bool
ImageHistoryMap map[string][]dockerimagetypes.HistoryResponseItem
}
const (
// Notice that if someday we also have minimum docker version requirement, this should also be updated.
fakeDockerVersion = "1.11.2"
fakeImageSize = 1024
// Docker prepends '/' to the container name.
dockerNamePrefix = "/"
)
func NewFakeDockerClient() *FakeDockerClient {
return &FakeDockerClient{
// Docker's API version does not include the patch number.
VersionInfo: dockertypes.Version{Version: fakeDockerVersion, APIVersion: strings.TrimSuffix(MinimumDockerAPIVersion, ".0")},
Errors: make(map[string]error),
ContainerMap: make(map[string]*dockertypes.ContainerJSON),
Clock: clock.RealClock{},
// default this to true, so that we trace calls, image pulls and container lifecycle
EnableTrace: true,
ImageInspects: make(map[string]*dockertypes.ImageInspect),
ImageIDsNeedingAuth: make(map[string]dockertypes.AuthConfig),
}
}
func (f *FakeDockerClient) WithClock(c clock.Clock) *FakeDockerClient {
f.Lock()
defer f.Unlock()
f.Clock = c
return f
}
func (f *FakeDockerClient) WithVersion(version, apiVersion string) *FakeDockerClient {
f.Lock()
defer f.Unlock()
f.VersionInfo = dockertypes.Version{Version: version, APIVersion: apiVersion}
return f
}
func (f *FakeDockerClient) WithTraceDisabled() *FakeDockerClient {
f.Lock()
defer f.Unlock()
f.EnableTrace = false
return f
}
func (f *FakeDockerClient) appendCalled(callDetail calledDetail) {
if f.EnableTrace {
f.called = append(f.called, callDetail)
}
}
func (f *FakeDockerClient) appendPulled(pull string) {
if f.EnableTrace {
f.pulled = append(f.pulled, pull)
}
}
func (f *FakeDockerClient) appendContainerTrace(traceCategory string, containerName string) {
if !f.EnableTrace {
return
}
switch traceCategory {
case "Created":
f.Created = append(f.Created, containerName)
case "Started":
f.Started = append(f.Started, containerName)
case "Stopped":
f.Stopped = append(f.Stopped, containerName)
case "Removed":
f.Removed = append(f.Removed, containerName)
}
}
func (f *FakeDockerClient) InjectError(fn string, err error) {
f.Lock()
defer f.Unlock()
f.Errors[fn] = err
}
func (f *FakeDockerClient) InjectErrors(errs map[string]error) {
f.Lock()
defer f.Unlock()
for fn, err := range errs {
f.Errors[fn] = err
}
}
func (f *FakeDockerClient) ClearErrors() {
f.Lock()
defer f.Unlock()
f.Errors = map[string]error{}
}
func (f *FakeDockerClient) ClearCalls() {
f.Lock()
defer f.Unlock()
f.called = []calledDetail{}
f.pulled = []string{}
f.Created = []string{}
f.Started = []string{}
f.Stopped = []string{}
f.Removed = []string{}
}
func (f *FakeDockerClient) getCalledNames() []string {
names := []string{}
for _, detail := range f.called {
names = append(names, detail.name)
}
return names
}
// Because the new data type returned by engine-api is too complex to manually initialize, we need a
// fake container which is easier to initialize.
type FakeContainer struct {
ID string
Name string
Running bool
ExitCode int
Pid int
CreatedAt time.Time
StartedAt time.Time
FinishedAt time.Time
Config *dockercontainer.Config
HostConfig *dockercontainer.HostConfig
}
// convertFakeContainer converts the fake container to real container
func convertFakeContainer(f *FakeContainer) *dockertypes.ContainerJSON {
if f.Config == nil {
f.Config = &dockercontainer.Config{}
}
if f.HostConfig == nil {
f.HostConfig = &dockercontainer.HostConfig{}
}
return &dockertypes.ContainerJSON{
ContainerJSONBase: &dockertypes.ContainerJSONBase{
ID: f.ID,
Name: f.Name,
Image: f.Config.Image,
State: &dockertypes.ContainerState{
Running: f.Running,
ExitCode: f.ExitCode,
Pid: f.Pid,
StartedAt: dockerTimestampToString(f.StartedAt),
FinishedAt: dockerTimestampToString(f.FinishedAt),
},
Created: dockerTimestampToString(f.CreatedAt),
HostConfig: f.HostConfig,
},
Config: f.Config,
NetworkSettings: &dockertypes.NetworkSettings{},
}
}
func (f *FakeDockerClient) SetFakeContainers(containers []*FakeContainer) {
f.Lock()
defer f.Unlock()
// Reset the lists and the map.
f.ContainerMap = map[string]*dockertypes.ContainerJSON{}
f.RunningContainerList = []dockertypes.Container{}
f.ExitedContainerList = []dockertypes.Container{}
for i := range containers {
c := containers[i]
f.ContainerMap[c.ID] = convertFakeContainer(c)
container := dockertypes.Container{
Names: []string{c.Name},
ID: c.ID,
}
if c.Config != nil {
container.Labels = c.Config.Labels
}
if c.Running {
f.RunningContainerList = append(f.RunningContainerList, container)
} else {
f.ExitedContainerList = append(f.ExitedContainerList, container)
}
}
}
func (f *FakeDockerClient) SetFakeRunningContainers(containers []*FakeContainer) {
for _, c := range containers {
c.Running = true
}
f.SetFakeContainers(containers)
}
func (f *FakeDockerClient) AssertCalls(calls []string) (err error) {
f.Lock()
defer f.Unlock()
if !reflect.DeepEqual(calls, f.getCalledNames()) {
err = fmt.Errorf("expected %#v, got %#v", calls, f.getCalledNames())
}
return
}
func (f *FakeDockerClient) AssertCallDetails(calls ...calledDetail) (err error) {
f.Lock()
defer f.Unlock()
if !reflect.DeepEqual(calls, f.called) {
err = fmt.Errorf("expected %#v, got %#v", calls, f.called)
}
return
}
// idsToNames converts container ids into names. The caller must hold the lock.
func (f *FakeDockerClient) idsToNames(ids []string) ([]string, error) {
names := []string{}
for _, id := range ids {
names = append(names, strings.TrimPrefix(f.ContainerMap[id].Name, dockerNamePrefix))
}
return names, nil
}
func (f *FakeDockerClient) AssertCreatedByNameWithOrder(created []string) error {
f.Lock()
defer f.Unlock()
actualCreated, err := f.idsToNames(f.Created)
if err != nil {
return err
}
if !reflect.DeepEqual(created, actualCreated) {
return fmt.Errorf("expected %#v, got %#v", created, actualCreated)
}
return nil
}
func (f *FakeDockerClient) AssertCreatedByName(created []string) error {
f.Lock()
defer f.Unlock()
actualCreated, err := f.idsToNames(f.Created)
if err != nil {
return err
}
return sortedStringSlicesEqual(created, actualCreated)
}
func (f *FakeDockerClient) AssertStoppedByName(stopped []string) error {
f.Lock()
defer f.Unlock()
actualStopped, err := f.idsToNames(f.Stopped)
if err != nil {
return err
}
return sortedStringSlicesEqual(stopped, actualStopped)
}
func (f *FakeDockerClient) AssertStopped(stopped []string) error {
f.Lock()
defer f.Unlock()
// Copy stopped to avoid modifying it.
actualStopped := append([]string{}, f.Stopped...)
return sortedStringSlicesEqual(stopped, actualStopped)
}
func (f *FakeDockerClient) AssertImagesPulled(pulled []string) error {
f.Lock()
defer f.Unlock()
// Copy pulled to avoid modifying it.
actualPulled := append([]string{}, f.ImagesPulled...)
return sortedStringSlicesEqual(pulled, actualPulled)
}
func (f *FakeDockerClient) AssertImagesPulledMsgs(expected []string) error {
f.Lock()
defer f.Unlock()
// Copy pulled to avoid modifying it.
actual := append([]string{}, f.pulled...)
return sortedStringSlicesEqual(expected, actual)
}
func sortedStringSlicesEqual(expected, actual []string) error {
sort.StringSlice(expected).Sort()
sort.StringSlice(actual).Sort()
if !reflect.DeepEqual(expected, actual) {
return fmt.Errorf("expected %#v, got %#v", expected, actual)
}
return nil
}
func (f *FakeDockerClient) popError(op string) error {
if f.Errors == nil {
return nil
}
err, ok := f.Errors[op]
if ok {
delete(f.Errors, op)
return err
}
return nil
}
// ListContainers is a test-spy implementation of Interface.ListContainers.
// It adds an entry "list" to the internal method call record.
func (f *FakeDockerClient) ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "list"})
err := f.popError("list")
containerList := append([]dockertypes.Container{}, f.RunningContainerList...)
if options.All {
// Although the container is not sorted, but the container with the same name should be in order,
// that is enough for us now.
// TODO(random-liu): Is a fully sorted array needed?
containerList = append(containerList, f.ExitedContainerList...)
}
// Filters containers with id, only support 1 id.
idFilters := options.Filters.Get("id")
if len(idFilters) != 0 {
var filtered []dockertypes.Container
for _, container := range containerList {
for _, idFilter := range idFilters {
if container.ID == idFilter {
filtered = append(filtered, container)
break
}
}
}
containerList = filtered
}
// Filters containers with status, only support 1 status.
statusFilters := options.Filters.Get("status")
if len(statusFilters) == 1 {
var filtered []dockertypes.Container
for _, container := range containerList {
for _, statusFilter := range statusFilters {
if toDockerContainerStatus(container.Status) == statusFilter {
filtered = append(filtered, container)
break
}
}
}
containerList = filtered
}
// Filters containers with label filter.
labelFilters := options.Filters.Get("label")
if len(labelFilters) != 0 {
var filtered []dockertypes.Container
for _, container := range containerList {
match := true
for _, labelFilter := range labelFilters {
kv := strings.Split(labelFilter, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("invalid label filter %q", labelFilter)
}
if container.Labels[kv[0]] != kv[1] {
match = false
break
}
}
if match {
filtered = append(filtered, container)
}
}
containerList = filtered
}
return containerList, err
}
func toDockerContainerStatus(state string) string {
switch {
case strings.HasPrefix(state, StatusCreatedPrefix):
return "created"
case strings.HasPrefix(state, StatusRunningPrefix):
return "running"
case strings.HasPrefix(state, StatusExitedPrefix):
return "exited"
default:
return "unknown"
}
}
// InspectContainer is a test-spy implementation of Interface.InspectContainer.
// It adds an entry "inspect" to the internal method call record.
func (f *FakeDockerClient) InspectContainer(id string) (*dockertypes.ContainerJSON, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "inspect_container"})
err := f.popError("inspect_container")
if container, ok := f.ContainerMap[id]; ok {
return container, err
}
if err != nil {
// Use the custom error if it exists.
return nil, err
}
return nil, fmt.Errorf("container %q not found", id)
}
// InspectContainerWithSize is a test-spy implementation of Interface.InspectContainerWithSize.
// It adds an entry "inspect" to the internal method call record.
func (f *FakeDockerClient) InspectContainerWithSize(id string) (*dockertypes.ContainerJSON, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "inspect_container_withsize"})
err := f.popError("inspect_container_withsize")
if container, ok := f.ContainerMap[id]; ok {
return container, err
}
if err != nil {
// Use the custom error if it exists.
return nil, err
}
return nil, fmt.Errorf("container %q not found", id)
}
// InspectImageByRef is a test-spy implementation of Interface.InspectImageByRef.
// It adds an entry "inspect" to the internal method call record.
func (f *FakeDockerClient) InspectImageByRef(name string) (*dockertypes.ImageInspect, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "inspect_image"})
if err := f.popError("inspect_image"); err != nil {
return nil, err
}
if result, ok := f.ImageInspects[name]; ok {
return result, nil
}
return nil, ImageNotFoundError{name}
}
// InspectImageByID is a test-spy implementation of Interface.InspectImageByID.
// It adds an entry "inspect" to the internal method call record.
func (f *FakeDockerClient) InspectImageByID(name string) (*dockertypes.ImageInspect, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "inspect_image"})
if err := f.popError("inspect_image"); err != nil {
return nil, err
}
if result, ok := f.ImageInspects[name]; ok {
return result, nil
}
return nil, ImageNotFoundError{name}
}
// Sleeps random amount of time with the normal distribution with given mean and stddev
// (in milliseconds), we never sleep less than cutOffMillis
func (f *FakeDockerClient) normalSleep(mean, stdDev, cutOffMillis int) {
if !f.EnableSleep {
return
}
cutoff := (time.Duration)(cutOffMillis) * time.Millisecond
delay := (time.Duration)(rand.NormFloat64()*float64(stdDev)+float64(mean)) * time.Millisecond
if delay < cutoff {
delay = cutoff
}
time.Sleep(delay)
}
// GetFakeContainerID generates a fake container id from container name with a hash.
func GetFakeContainerID(name string) string {
hash := fnv.New64a()
hash.Write([]byte(name))
return strconv.FormatUint(hash.Sum64(), 16)
}
// CreateContainer is a test-spy implementation of Interface.CreateContainer.
// It adds an entry "create" to the internal method call record.
func (f *FakeDockerClient) CreateContainer(c dockertypes.ContainerCreateConfig) (*dockercontainer.ContainerCreateCreatedBody, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "create"})
if err := f.popError("create"); err != nil {
return nil, err
}
// This is not a very good fake. We'll just add this container's name to the list.
name := dockerNamePrefix + c.Name
id := GetFakeContainerID(name)
f.appendContainerTrace("Created", id)
timestamp := f.Clock.Now()
// The newest container should be in front, because we assume so in GetPodStatus()
f.RunningContainerList = append([]dockertypes.Container{
{ID: id, Names: []string{name}, Image: c.Config.Image, Created: timestamp.Unix(), State: StatusCreatedPrefix, Labels: c.Config.Labels},
}, f.RunningContainerList...)
f.ContainerMap[id] = convertFakeContainer(&FakeContainer{
ID: id, Name: name, Config: c.Config, HostConfig: c.HostConfig, CreatedAt: timestamp})
f.normalSleep(100, 25, 25)
return &dockercontainer.ContainerCreateCreatedBody{ID: id}, nil
}
// StartContainer is a test-spy implementation of Interface.StartContainer.
// It adds an entry "start" to the internal method call record.
func (f *FakeDockerClient) StartContainer(id string) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "start"})
if err := f.popError("start"); err != nil {
return err
}
f.appendContainerTrace("Started", id)
container, ok := f.ContainerMap[id]
if container.HostConfig.NetworkMode.IsContainer() {
hostContainerID := container.HostConfig.NetworkMode.ConnectedContainer()
found := false
for _, container := range f.RunningContainerList {
if container.ID == hostContainerID {
found = true
}
}
if !found {
return fmt.Errorf("failed to start container \"%s\": Error response from daemon: cannot join network of a non running container: %s", id, hostContainerID)
}
}
timestamp := f.Clock.Now()
if !ok {
container = convertFakeContainer(&FakeContainer{ID: id, Name: id, CreatedAt: timestamp})
}
container.State.Running = true
container.State.Pid = os.Getpid()
container.State.StartedAt = dockerTimestampToString(timestamp)
container.NetworkSettings.IPAddress = "2.3.4.5"
f.ContainerMap[id] = container
f.updateContainerStatus(id, StatusRunningPrefix)
f.normalSleep(200, 50, 50)
return nil
}
// StopContainer is a test-spy implementation of Interface.StopContainer.
// It adds an entry "stop" to the internal method call record.
func (f *FakeDockerClient) StopContainer(id string, timeout time.Duration) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "stop"})
if err := f.popError("stop"); err != nil {
return err
}
f.appendContainerTrace("Stopped", id)
// Container status should be Updated before container moved to ExitedContainerList
f.updateContainerStatus(id, StatusExitedPrefix)
var newList []dockertypes.Container
for _, container := range f.RunningContainerList {
if container.ID == id {
// The newest exited container should be in front. Because we assume so in GetPodStatus()
f.ExitedContainerList = append([]dockertypes.Container{container}, f.ExitedContainerList...)
continue
}
newList = append(newList, container)
}
f.RunningContainerList = newList
container, ok := f.ContainerMap[id]
if !ok {
container = convertFakeContainer(&FakeContainer{
ID: id,
Name: id,
Running: false,
StartedAt: time.Now().Add(-time.Second),
FinishedAt: time.Now(),
})
} else {
container.State.FinishedAt = dockerTimestampToString(f.Clock.Now())
container.State.Running = false
}
f.ContainerMap[id] = container
f.normalSleep(200, 50, 50)
return nil
}
func (f *FakeDockerClient) RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "remove"})
err := f.popError("remove")
if err != nil {
return err
}
for i := range f.ExitedContainerList {
if f.ExitedContainerList[i].ID == id {
delete(f.ContainerMap, id)
f.ExitedContainerList = append(f.ExitedContainerList[:i], f.ExitedContainerList[i+1:]...)
f.appendContainerTrace("Removed", id)
return nil
}
}
for i := range f.RunningContainerList {
// allow removal of running containers which are not running
if f.RunningContainerList[i].ID == id && !f.ContainerMap[id].State.Running {
delete(f.ContainerMap, id)
f.RunningContainerList = append(f.RunningContainerList[:i], f.RunningContainerList[i+1:]...)
f.appendContainerTrace("Removed", id)
return nil
}
}
// To be a good fake, report error if container is not stopped.
return fmt.Errorf("container not stopped")
}
func (f *FakeDockerClient) UpdateContainerResources(id string, updateConfig dockercontainer.UpdateConfig) error {
return nil
}
// Logs is a test-spy implementation of Interface.Logs.
// It adds an entry "logs" to the internal method call record.
func (f *FakeDockerClient) Logs(id string, opts dockertypes.ContainerLogsOptions, sopts StreamOptions) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "logs"})
return f.popError("logs")
}
func (f *FakeDockerClient) isAuthorizedForImage(image string, auth dockertypes.AuthConfig) bool {
if reqd, exists := f.ImageIDsNeedingAuth[image]; !exists {
return true // no auth needed
} else {
return auth.Username == reqd.Username && auth.Password == reqd.Password
}
}
// PullImage is a test-spy implementation of Interface.PullImage.
// It adds an entry "pull" to the internal method call record.
func (f *FakeDockerClient) PullImage(image string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "pull"})
err := f.popError("pull")
if err == nil {
if !f.isAuthorizedForImage(image, auth) {
return ImageNotFoundError{ID: image}
}
authJson, _ := json.Marshal(auth)
inspect := createImageInspectFromRef(image)
f.ImageInspects[image] = inspect
f.appendPulled(fmt.Sprintf("%s using %s", image, string(authJson)))
f.Images = append(f.Images, *createImageFromImageInspect(*inspect))
f.ImagesPulled = append(f.ImagesPulled, image)
}
return err
}
func (f *FakeDockerClient) Version() (*dockertypes.Version, error) {
f.Lock()
defer f.Unlock()
v := f.VersionInfo
return &v, f.popError("version")
}
func (f *FakeDockerClient) Info() (*dockertypes.Info, error) {
return &f.Information, nil
}
func (f *FakeDockerClient) CreateExec(id string, opts dockertypes.ExecConfig) (*dockertypes.IDResponse, error) {
f.Lock()
defer f.Unlock()
f.execCmd = opts.Cmd
f.appendCalled(calledDetail{name: "create_exec"})
return &dockertypes.IDResponse{ID: "12345678"}, nil
}
func (f *FakeDockerClient) StartExec(startExec string, opts dockertypes.ExecStartCheck, sopts StreamOptions) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "start_exec"})
return nil
}
func (f *FakeDockerClient) AttachToContainer(id string, opts dockertypes.ContainerAttachOptions, sopts StreamOptions) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "attach"})
return nil
}
func (f *FakeDockerClient) InspectExec(id string) (*dockertypes.ContainerExecInspect, error) {
return f.ExecInspect, f.popError("inspect_exec")
}
func (f *FakeDockerClient) ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.ImageSummary, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "list_images"})
err := f.popError("list_images")
return f.Images, err
}
func (f *FakeDockerClient) RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "remove_image", arguments: []interface{}{image, opts}})
err := f.popError("remove_image")
if err == nil {
for i := range f.Images {
if f.Images[i].ID == image {
f.Images = append(f.Images[:i], f.Images[i+1:]...)
break
}
}
}
return []dockertypes.ImageDeleteResponseItem{{Deleted: image}}, err
}
func (f *FakeDockerClient) InjectImages(images []dockertypes.ImageSummary) {
f.Lock()
defer f.Unlock()
f.Images = append(f.Images, images...)
for _, i := range images {
f.ImageInspects[i.ID] = createImageInspectFromImage(i)
}
}
func (f *FakeDockerClient) MakeImagesPrivate(images []dockertypes.ImageSummary, auth dockertypes.AuthConfig) {
f.Lock()
defer f.Unlock()
for _, i := range images {
f.ImageIDsNeedingAuth[i.ID] = auth
}
}
func (f *FakeDockerClient) ResetImages() {
f.Lock()
defer f.Unlock()
f.Images = []dockertypes.ImageSummary{}
f.ImageInspects = make(map[string]*dockertypes.ImageInspect)
f.ImageIDsNeedingAuth = make(map[string]dockertypes.AuthConfig)
}
func (f *FakeDockerClient) InjectImageInspects(inspects []dockertypes.ImageInspect) {
f.Lock()
defer f.Unlock()
for _, i := range inspects {
f.Images = append(f.Images, *createImageFromImageInspect(i))
f.ImageInspects[i.ID] = &i
}
}
func (f *FakeDockerClient) updateContainerStatus(id, status string) {
for i := range f.RunningContainerList {
if f.RunningContainerList[i].ID == id {
f.RunningContainerList[i].Status = status
}
}
}
func (f *FakeDockerClient) ResizeExecTTY(id string, height, width uint) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "resize_exec"})
return nil
}
func (f *FakeDockerClient) ResizeContainerTTY(id string, height, width uint) error {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "resize_container"})
return nil
}
func createImageInspectFromRef(ref string) *dockertypes.ImageInspect {
return &dockertypes.ImageInspect{
ID: ref,
RepoTags: []string{ref},
// Image size is required to be non-zero for CRI integration.
VirtualSize: fakeImageSize,
Size: fakeImageSize,
Config: &dockercontainer.Config{},
}
}
func createImageInspectFromImage(image dockertypes.ImageSummary) *dockertypes.ImageInspect {
return &dockertypes.ImageInspect{
ID: image.ID,
RepoTags: image.RepoTags,
// Image size is required to be non-zero for CRI integration.
VirtualSize: fakeImageSize,
Size: fakeImageSize,
Config: &dockercontainer.Config{},
}
}
func createImageFromImageInspect(inspect dockertypes.ImageInspect) *dockertypes.ImageSummary {
return &dockertypes.ImageSummary{
ID: inspect.ID,
RepoTags: inspect.RepoTags,
// Image size is required to be non-zero for CRI integration.
VirtualSize: fakeImageSize,
Size: fakeImageSize,
}
}
// dockerTimestampToString converts the timestamp to string
func dockerTimestampToString(t time.Time) string {
return t.Format(time.RFC3339Nano)
}
func (f *FakeDockerClient) ImageHistory(id string) ([]dockerimagetypes.HistoryResponseItem, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "image_history"})
history := f.ImageHistoryMap[id]
return history, nil
}
func (f *FakeDockerClient) InjectImageHistory(data map[string][]dockerimagetypes.HistoryResponseItem) {
f.Lock()
defer f.Unlock()
f.ImageHistoryMap = data
}
// FakeDockerPuller is meant to be a simple wrapper around FakeDockerClient.
// Please do not add more functionalities to it.
type FakeDockerPuller struct {
client Interface
}
func (f *FakeDockerPuller) Pull(image string, _ []v1.Secret) error {
return f.client.PullImage(image, dockertypes.AuthConfig{}, dockertypes.ImagePullOptions{})
}
func (f *FakeDockerPuller) GetImageRef(image string) (string, error) {
_, err := f.client.InspectImageByRef(image)
if err != nil && IsImageNotFoundError(err) {
return "", nil
}
return image, err
}
func (f *FakeDockerClient) GetContainerStats(id string) (*dockertypes.StatsJSON, error) {
f.Lock()
defer f.Unlock()
f.appendCalled(calledDetail{name: "getContainerStats"})
return nil, fmt.Errorf("not implemented")
}

View File

@ -0,0 +1,172 @@
/*
Copyright 2014 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 libdocker
import (
"strings"
"time"
dockerref "github.com/docker/distribution/reference"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
godigest "github.com/opencontainers/go-digest"
)
// ParseDockerTimestamp parses the timestamp returned by Interface from string to time.Time
func ParseDockerTimestamp(s string) (time.Time, error) {
// Timestamp returned by Docker is in time.RFC3339Nano format.
return time.Parse(time.RFC3339Nano, s)
}
// matchImageTagOrSHA checks if the given image specifier is a valid image ref,
// and that it matches the given image. It should fail on things like image IDs
// (config digests) and other digest-only references, but succeed on image names
// (`foo`), tag references (`foo:bar`), and manifest digest references
// (`foo@sha256:xyz`).
func matchImageTagOrSHA(inspected dockertypes.ImageInspect, image string) bool {
// The image string follows the grammar specified here
// https://github.com/docker/distribution/blob/master/reference/reference.go#L4
named, err := dockerref.ParseNormalizedNamed(image)
if err != nil {
glog.V(4).Infof("couldn't parse image reference %q: %v", image, err)
return false
}
_, isTagged := named.(dockerref.Tagged)
digest, isDigested := named.(dockerref.Digested)
if !isTagged && !isDigested {
// No Tag or SHA specified, so just return what we have
return true
}
if isTagged {
// Check the RepoTags for a match.
for _, tag := range inspected.RepoTags {
// An image name (without the tag/digest) can be [hostname '/'] component ['/' component]*
// Because either the RepoTag or the name *may* contain the
// hostname or not, we only check for the suffix match.
if strings.HasSuffix(image, tag) || strings.HasSuffix(tag, image) {
return true
} else {
// TODO: We need to remove this hack when project atomic based
// docker distro(s) like centos/fedora/rhel image fix problems on
// their end.
// Say the tag is "docker.io/busybox:latest"
// and the image is "docker.io/library/busybox:latest"
t, err := dockerref.ParseNormalizedNamed(tag)
if err != nil {
continue
}
// the parsed/normalized tag will look like
// reference.taggedReference {
// namedRepository: reference.repository {
// domain: "docker.io",
// path: "library/busybox"
// },
// tag: "latest"
// }
// If it does not have tags then we bail out
t2, ok := t.(dockerref.Tagged)
if !ok {
continue
}
// normalized tag would look like "docker.io/library/busybox:latest"
// note the library get added in the string
normalizedTag := t2.String()
if normalizedTag == "" {
continue
}
if strings.HasSuffix(image, normalizedTag) || strings.HasSuffix(normalizedTag, image) {
return true
}
}
}
}
if isDigested {
for _, repoDigest := range inspected.RepoDigests {
named, err := dockerref.ParseNormalizedNamed(repoDigest)
if err != nil {
glog.V(4).Infof("couldn't parse image RepoDigest reference %q: %v", repoDigest, err)
continue
}
if d, isDigested := named.(dockerref.Digested); isDigested {
if digest.Digest().Algorithm().String() == d.Digest().Algorithm().String() &&
digest.Digest().Hex() == d.Digest().Hex() {
return true
}
}
}
// process the ID as a digest
id, err := godigest.Parse(inspected.ID)
if err != nil {
glog.V(4).Infof("couldn't parse image ID reference %q: %v", id, err)
return false
}
if digest.Digest().Algorithm().String() == id.Algorithm().String() && digest.Digest().Hex() == id.Hex() {
return true
}
}
glog.V(4).Infof("Inspected image (%q) does not match %s", inspected.ID, image)
return false
}
// matchImageIDOnly checks that the given image specifier is a digest-only
// reference, and that it matches the given image.
func matchImageIDOnly(inspected dockertypes.ImageInspect, image string) bool {
// If the image ref is literally equal to the inspected image's ID,
// just return true here (this might be the case for Docker 1.9,
// where we won't have a digest for the ID)
if inspected.ID == image {
return true
}
// Otherwise, we should try actual parsing to be more correct
ref, err := dockerref.Parse(image)
if err != nil {
glog.V(4).Infof("couldn't parse image reference %q: %v", image, err)
return false
}
digest, isDigested := ref.(dockerref.Digested)
if !isDigested {
glog.V(4).Infof("the image reference %q was not a digest reference", image)
return false
}
id, err := godigest.Parse(inspected.ID)
if err != nil {
glog.V(4).Infof("couldn't parse image ID reference %q: %v", id, err)
return false
}
if digest.Digest().Algorithm().String() == id.Algorithm().String() && digest.Digest().Hex() == id.Hex() {
return true
}
glog.V(4).Infof("The reference %s does not directly refer to the given image's ID (%q)", image, inspected.ID)
return false
}
// isImageNotFoundError returns whether the err is caused by image not found in docker
// TODO: Use native error tester once ImageNotFoundError is supported in docker-engine client(eg. ImageRemove())
func isImageNotFoundError(err error) bool {
if err != nil {
return strings.Contains(err.Error(), "No such image:")
}
return false
}

View File

@ -0,0 +1,270 @@
/*
Copyright 2014 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 libdocker
import (
"fmt"
"testing"
dockertypes "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func TestMatchImageTagOrSHA(t *testing.T) {
for i, testCase := range []struct {
Inspected dockertypes.ImageInspect
Image string
Output bool
}{
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"ubuntu:latest"}},
Image: "ubuntu",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"ubuntu:14.04"}},
Image: "ubuntu:latest",
Output: false,
},
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"colemickens/hyperkube-amd64:217.9beff63"}},
Image: "colemickens/hyperkube-amd64:217.9beff63",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"colemickens/hyperkube-amd64:217.9beff63"}},
Image: "docker.io/colemickens/hyperkube-amd64:217.9beff63",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"docker.io/kubernetes/pause:latest"}},
Image: "kubernetes/pause:latest",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208f7a29005",
Output: false,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208",
Output: false,
},
{
// mismatched ID is ignored
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:0000f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
Output: false,
},
{
// invalid digest is ignored
Inspected: dockertypes.ImageInspect{
ID: "sha256:unparseable",
},
Image: "myimage@sha256:unparseable",
Output: false,
},
{
// v1 schema images can be pulled in one format and returned in another
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoTags: []string{"docker.io/busybox:latest"},
},
Image: "docker.io/library/busybox:latest",
Output: true,
},
{
// RepoDigest match is is required
Inspected: dockertypes.ImageInspect{
ID: "",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:000084acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: false,
},
{
// RepoDigest match is allowed
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: true,
},
{
// RepoDigest and ID are checked
Inspected: dockertypes.ImageInspect{
ID: "sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: true,
},
{
// unparseable RepoDigests are skipped
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{
"centos/ruby-23-centos7@sha256:unparseable",
"docker.io/centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: true,
},
{
// unparseable RepoDigest is ignored
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:unparseable"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: false,
},
{
// unparseable image digest is ignored
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:unparseable"},
},
Image: "centos/ruby-23-centos7@sha256:unparseable",
Output: false,
},
{
// prefix match is rejected for ID and RepoDigest
Inspected: dockertypes.ImageInspect{
ID: "sha256:unparseable",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:unparseable"},
},
Image: "sha256:unparseable",
Output: false,
},
{
// possible SHA prefix match is rejected for ID and RepoDigest because it is not in the named format
Inspected: dockertypes.ImageInspect{
ID: "sha256:0000f247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"docker.io/centos/ruby-23-centos7@sha256:0000f247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227"},
},
Image: "sha256:0000",
Output: false,
},
} {
match := matchImageTagOrSHA(testCase.Inspected, testCase.Image)
assert.Equal(t, testCase.Output, match, testCase.Image+fmt.Sprintf(" is not a match (%d)", i))
}
}
func TestMatchImageIDOnly(t *testing.T) {
for i, testCase := range []struct {
Inspected dockertypes.ImageInspect
Image string
Output bool
}{
// shouldn't match names or tagged names
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"ubuntu:latest"}},
Image: "ubuntu",
Output: false,
},
{
Inspected: dockertypes.ImageInspect{RepoTags: []string{"colemickens/hyperkube-amd64:217.9beff63"}},
Image: "colemickens/hyperkube-amd64:217.9beff63",
Output: false,
},
// should match name@digest refs if they refer to the image ID (but only the full ID)
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
Output: true,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208f7a29005",
Output: false,
},
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:2208",
Output: false,
},
// should match when the IDs are literally the same
{
Inspected: dockertypes.ImageInspect{
ID: "foobar",
},
Image: "foobar",
Output: true,
},
// shouldn't match mismatched IDs
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:2208f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
},
Image: "myimage@sha256:0000f7a29005d226d1ee33a63e33af1f47af6156c740d7d23c7948e8d282d53d",
Output: false,
},
// shouldn't match invalid IDs or refs
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:unparseable",
},
Image: "myimage@sha256:unparseable",
Output: false,
},
// shouldn't match against repo digests
{
Inspected: dockertypes.ImageInspect{
ID: "sha256:9bbdf247c91345f0789c10f50a57e36a667af1189687ad1de88a6243d05a2227",
RepoDigests: []string{"centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf"},
},
Image: "centos/ruby-23-centos7@sha256:940584acbbfb0347272112d2eb95574625c0c60b4e2fdadb139de5859cf754bf",
Output: false,
},
} {
match := matchImageIDOnly(testCase.Inspected, testCase.Image)
assert.Equal(t, testCase.Output, match, fmt.Sprintf("%s is not a match (%d)", testCase.Image, i))
}
}

View File

@ -0,0 +1,272 @@
/*
Copyright 2015 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 libdocker
import (
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerimagetypes "github.com/docker/docker/api/types/image"
"k8s.io/kubernetes/pkg/kubelet/dockershim/metrics"
)
// instrumentedInterface wraps the Interface and records the operations
// and errors metrics.
type instrumentedInterface struct {
client Interface
}
// NewInstrumentedInterface creates an instrumented Interface from an existing Interface.
func NewInstrumentedInterface(dockerClient Interface) Interface {
return instrumentedInterface{
client: dockerClient,
}
}
// recordOperation records the duration of the operation.
func recordOperation(operation string, start time.Time) {
metrics.DockerOperations.WithLabelValues(operation).Inc()
metrics.DockerOperationsLatency.WithLabelValues(operation).Observe(metrics.SinceInMicroseconds(start))
}
// recordError records error for metric if an error occurred.
func recordError(operation string, err error) {
if err != nil {
if _, ok := err.(operationTimeout); ok {
metrics.DockerOperationsTimeout.WithLabelValues(operation).Inc()
}
// Docker operation timeout error is also a docker error, so we don't add else here.
metrics.DockerOperationsErrors.WithLabelValues(operation).Inc()
}
}
func (in instrumentedInterface) ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error) {
const operation = "list_containers"
defer recordOperation(operation, time.Now())
out, err := in.client.ListContainers(options)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) InspectContainer(id string) (*dockertypes.ContainerJSON, error) {
const operation = "inspect_container"
defer recordOperation(operation, time.Now())
out, err := in.client.InspectContainer(id)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) InspectContainerWithSize(id string) (*dockertypes.ContainerJSON, error) {
const operation = "inspect_container_withsize"
defer recordOperation(operation, time.Now())
out, err := in.client.InspectContainerWithSize(id)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) CreateContainer(opts dockertypes.ContainerCreateConfig) (*dockercontainer.ContainerCreateCreatedBody, error) {
const operation = "create_container"
defer recordOperation(operation, time.Now())
out, err := in.client.CreateContainer(opts)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) StartContainer(id string) error {
const operation = "start_container"
defer recordOperation(operation, time.Now())
err := in.client.StartContainer(id)
recordError(operation, err)
return err
}
func (in instrumentedInterface) StopContainer(id string, timeout time.Duration) error {
const operation = "stop_container"
defer recordOperation(operation, time.Now())
err := in.client.StopContainer(id, timeout)
recordError(operation, err)
return err
}
func (in instrumentedInterface) RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error {
const operation = "remove_container"
defer recordOperation(operation, time.Now())
err := in.client.RemoveContainer(id, opts)
recordError(operation, err)
return err
}
func (in instrumentedInterface) UpdateContainerResources(id string, updateConfig dockercontainer.UpdateConfig) error {
const operation = "update_container"
defer recordOperation(operation, time.Now())
err := in.client.UpdateContainerResources(id, updateConfig)
recordError(operation, err)
return err
}
func (in instrumentedInterface) InspectImageByRef(image string) (*dockertypes.ImageInspect, error) {
const operation = "inspect_image"
defer recordOperation(operation, time.Now())
out, err := in.client.InspectImageByRef(image)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) InspectImageByID(image string) (*dockertypes.ImageInspect, error) {
const operation = "inspect_image"
defer recordOperation(operation, time.Now())
out, err := in.client.InspectImageByID(image)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.ImageSummary, error) {
const operation = "list_images"
defer recordOperation(operation, time.Now())
out, err := in.client.ListImages(opts)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) PullImage(imageID string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error {
const operation = "pull_image"
defer recordOperation(operation, time.Now())
err := in.client.PullImage(imageID, auth, opts)
recordError(operation, err)
return err
}
func (in instrumentedInterface) RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) {
const operation = "remove_image"
defer recordOperation(operation, time.Now())
imageDelete, err := in.client.RemoveImage(image, opts)
recordError(operation, err)
return imageDelete, err
}
func (in instrumentedInterface) Logs(id string, opts dockertypes.ContainerLogsOptions, sopts StreamOptions) error {
const operation = "logs"
defer recordOperation(operation, time.Now())
err := in.client.Logs(id, opts, sopts)
recordError(operation, err)
return err
}
func (in instrumentedInterface) Version() (*dockertypes.Version, error) {
const operation = "version"
defer recordOperation(operation, time.Now())
out, err := in.client.Version()
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) Info() (*dockertypes.Info, error) {
const operation = "info"
defer recordOperation(operation, time.Now())
out, err := in.client.Info()
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) CreateExec(id string, opts dockertypes.ExecConfig) (*dockertypes.IDResponse, error) {
const operation = "create_exec"
defer recordOperation(operation, time.Now())
out, err := in.client.CreateExec(id, opts)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) StartExec(startExec string, opts dockertypes.ExecStartCheck, sopts StreamOptions) error {
const operation = "start_exec"
defer recordOperation(operation, time.Now())
err := in.client.StartExec(startExec, opts, sopts)
recordError(operation, err)
return err
}
func (in instrumentedInterface) InspectExec(id string) (*dockertypes.ContainerExecInspect, error) {
const operation = "inspect_exec"
defer recordOperation(operation, time.Now())
out, err := in.client.InspectExec(id)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) AttachToContainer(id string, opts dockertypes.ContainerAttachOptions, sopts StreamOptions) error {
const operation = "attach"
defer recordOperation(operation, time.Now())
err := in.client.AttachToContainer(id, opts, sopts)
recordError(operation, err)
return err
}
func (in instrumentedInterface) ImageHistory(id string) ([]dockerimagetypes.HistoryResponseItem, error) {
const operation = "image_history"
defer recordOperation(operation, time.Now())
out, err := in.client.ImageHistory(id)
recordError(operation, err)
return out, err
}
func (in instrumentedInterface) ResizeExecTTY(id string, height, width uint) error {
const operation = "resize_exec"
defer recordOperation(operation, time.Now())
err := in.client.ResizeExecTTY(id, height, width)
recordError(operation, err)
return err
}
func (in instrumentedInterface) ResizeContainerTTY(id string, height, width uint) error {
const operation = "resize_container"
defer recordOperation(operation, time.Now())
err := in.client.ResizeContainerTTY(id, height, width)
recordError(operation, err)
return err
}
func (in instrumentedInterface) GetContainerStats(id string) (*dockertypes.StatsJSON, error) {
const operation = "stats"
defer recordOperation(operation, time.Now())
out, err := in.client.GetContainerStats(id)
recordError(operation, err)
return out, err
}

View File

@ -0,0 +1,678 @@
/*
Copyright 2016 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 libdocker
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"regexp"
"sync"
"time"
"github.com/golang/glog"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockerimagetypes "github.com/docker/docker/api/types/image"
dockerapi "github.com/docker/docker/client"
dockermessage "github.com/docker/docker/pkg/jsonmessage"
dockerstdcopy "github.com/docker/docker/pkg/stdcopy"
"golang.org/x/net/context"
)
// kubeDockerClient is a wrapped layer of docker client for kubelet internal use. This layer is added to:
// 1) Redirect stream for exec and attach operations.
// 2) Wrap the context in this layer to make the Interface cleaner.
type kubeDockerClient struct {
// timeout is the timeout of short running docker operations.
timeout time.Duration
// If no pulling progress is made before imagePullProgressDeadline, the image pulling will be cancelled.
// Docker reports image progress for every 512kB block, so normally there shouldn't be too long interval
// between progress updates.
imagePullProgressDeadline time.Duration
client *dockerapi.Client
}
// Make sure that kubeDockerClient implemented the Interface.
var _ Interface = &kubeDockerClient{}
// There are 2 kinds of docker operations categorized by running time:
// * Long running operation: The long running operation could run for arbitrary long time, and the running time
// usually depends on some uncontrollable factors. These operations include: PullImage, Logs, StartExec, AttachToContainer.
// * Non-long running operation: Given the maximum load of the system, the non-long running operation should finish
// in expected and usually short time. These include all other operations.
// kubeDockerClient only applies timeout on non-long running operations.
const (
// defaultTimeout is the default timeout of short running docker operations.
// Value is slightly offset from 2 minutes to make timeouts due to this
// constant recognizable.
defaultTimeout = 2*time.Minute - 1*time.Second
// defaultShmSize is the default ShmSize to use (in bytes) if not specified.
defaultShmSize = int64(1024 * 1024 * 64)
// defaultImagePullingProgressReportInterval is the default interval of image pulling progress reporting.
defaultImagePullingProgressReportInterval = 10 * time.Second
)
// newKubeDockerClient creates an kubeDockerClient from an existing docker client. If requestTimeout is 0,
// defaultTimeout will be applied.
func newKubeDockerClient(dockerClient *dockerapi.Client, requestTimeout, imagePullProgressDeadline time.Duration) Interface {
if requestTimeout == 0 {
requestTimeout = defaultTimeout
}
k := &kubeDockerClient{
client: dockerClient,
timeout: requestTimeout,
imagePullProgressDeadline: imagePullProgressDeadline,
}
// Notice that this assumes that docker is running before kubelet is started.
v, err := k.Version()
if err != nil {
glog.Errorf("failed to retrieve docker version: %v", err)
glog.Warningf("Using empty version for docker client, this may sometimes cause compatibility issue.")
} else {
// Update client version with real api version.
dockerClient.NegotiateAPIVersionPing(dockertypes.Ping{APIVersion: v.APIVersion})
}
return k
}
func (d *kubeDockerClient) ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
containers, err := d.client.ContainerList(ctx, options)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return containers, nil
}
func (d *kubeDockerClient) InspectContainer(id string) (*dockertypes.ContainerJSON, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
containerJSON, err := d.client.ContainerInspect(ctx, id)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &containerJSON, nil
}
// InspectContainerWithSize is currently only used for Windows container stats
func (d *kubeDockerClient) InspectContainerWithSize(id string) (*dockertypes.ContainerJSON, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
// Inspects the container including the fields SizeRw and SizeRootFs.
containerJSON, _, err := d.client.ContainerInspectWithRaw(ctx, id, true)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &containerJSON, nil
}
func (d *kubeDockerClient) CreateContainer(opts dockertypes.ContainerCreateConfig) (*dockercontainer.ContainerCreateCreatedBody, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
// we provide an explicit default shm size as to not depend on docker daemon.
// TODO: evaluate exposing this as a knob in the API
if opts.HostConfig != nil && opts.HostConfig.ShmSize <= 0 {
opts.HostConfig.ShmSize = defaultShmSize
}
createResp, err := d.client.ContainerCreate(ctx, opts.Config, opts.HostConfig, opts.NetworkingConfig, opts.Name)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &createResp, nil
}
func (d *kubeDockerClient) StartContainer(id string) error {
ctx, cancel := d.getTimeoutContext()
defer cancel()
err := d.client.ContainerStart(ctx, id, dockertypes.ContainerStartOptions{})
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
// Stopping an already stopped container will not cause an error in dockerapi.
func (d *kubeDockerClient) StopContainer(id string, timeout time.Duration) error {
ctx, cancel := d.getCustomTimeoutContext(timeout)
defer cancel()
err := d.client.ContainerStop(ctx, id, &timeout)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
func (d *kubeDockerClient) RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error {
ctx, cancel := d.getTimeoutContext()
defer cancel()
err := d.client.ContainerRemove(ctx, id, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
func (d *kubeDockerClient) UpdateContainerResources(id string, updateConfig dockercontainer.UpdateConfig) error {
ctx, cancel := d.getTimeoutContext()
defer cancel()
_, err := d.client.ContainerUpdate(ctx, id, updateConfig)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
func (d *kubeDockerClient) inspectImageRaw(ref string) (*dockertypes.ImageInspect, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, _, err := d.client.ImageInspectWithRaw(ctx, ref)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
if dockerapi.IsErrImageNotFound(err) {
err = ImageNotFoundError{ID: ref}
}
return nil, err
}
return &resp, nil
}
func (d *kubeDockerClient) InspectImageByID(imageID string) (*dockertypes.ImageInspect, error) {
resp, err := d.inspectImageRaw(imageID)
if err != nil {
return nil, err
}
if !matchImageIDOnly(*resp, imageID) {
return nil, ImageNotFoundError{ID: imageID}
}
return resp, nil
}
func (d *kubeDockerClient) InspectImageByRef(imageRef string) (*dockertypes.ImageInspect, error) {
resp, err := d.inspectImageRaw(imageRef)
if err != nil {
return nil, err
}
if !matchImageTagOrSHA(*resp, imageRef) {
return nil, ImageNotFoundError{ID: imageRef}
}
return resp, nil
}
func (d *kubeDockerClient) ImageHistory(id string) ([]dockerimagetypes.HistoryResponseItem, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.ImageHistory(ctx, id)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
return resp, err
}
func (d *kubeDockerClient) ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.ImageSummary, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
images, err := d.client.ImageList(ctx, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return images, nil
}
func base64EncodeAuth(auth dockertypes.AuthConfig) (string, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(auth); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}
// progress is a wrapper of dockermessage.JSONMessage with a lock protecting it.
type progress struct {
sync.RWMutex
// message stores the latest docker json message.
message *dockermessage.JSONMessage
// timestamp of the latest update.
timestamp time.Time
}
func newProgress() *progress {
return &progress{timestamp: time.Now()}
}
func (p *progress) set(msg *dockermessage.JSONMessage) {
p.Lock()
defer p.Unlock()
p.message = msg
p.timestamp = time.Now()
}
func (p *progress) get() (string, time.Time) {
p.RLock()
defer p.RUnlock()
if p.message == nil {
return "No progress", p.timestamp
}
// The following code is based on JSONMessage.Display
var prefix string
if p.message.ID != "" {
prefix = fmt.Sprintf("%s: ", p.message.ID)
}
if p.message.Progress == nil {
return fmt.Sprintf("%s%s", prefix, p.message.Status), p.timestamp
}
return fmt.Sprintf("%s%s %s", prefix, p.message.Status, p.message.Progress.String()), p.timestamp
}
// progressReporter keeps the newest image pulling progress and periodically report the newest progress.
type progressReporter struct {
*progress
image string
cancel context.CancelFunc
stopCh chan struct{}
imagePullProgressDeadline time.Duration
}
// newProgressReporter creates a new progressReporter for specific image with specified reporting interval
func newProgressReporter(image string, cancel context.CancelFunc, imagePullProgressDeadline time.Duration) *progressReporter {
return &progressReporter{
progress: newProgress(),
image: image,
cancel: cancel,
stopCh: make(chan struct{}),
imagePullProgressDeadline: imagePullProgressDeadline,
}
}
// start starts the progressReporter
func (p *progressReporter) start() {
go func() {
ticker := time.NewTicker(defaultImagePullingProgressReportInterval)
defer ticker.Stop()
for {
// TODO(random-liu): Report as events.
select {
case <-ticker.C:
progress, timestamp := p.progress.get()
// If there is no progress for p.imagePullProgressDeadline, cancel the operation.
if time.Now().Sub(timestamp) > p.imagePullProgressDeadline {
glog.Errorf("Cancel pulling image %q because of no progress for %v, latest progress: %q", p.image, p.imagePullProgressDeadline, progress)
p.cancel()
return
}
glog.V(2).Infof("Pulling image %q: %q", p.image, progress)
case <-p.stopCh:
progress, _ := p.progress.get()
glog.V(2).Infof("Stop pulling image %q: %q", p.image, progress)
return
}
}
}()
}
// stop stops the progressReporter
func (p *progressReporter) stop() {
close(p.stopCh)
}
func (d *kubeDockerClient) PullImage(image string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error {
// RegistryAuth is the base64 encoded credentials for the registry
base64Auth, err := base64EncodeAuth(auth)
if err != nil {
return err
}
opts.RegistryAuth = base64Auth
ctx, cancel := d.getCancelableContext()
defer cancel()
resp, err := d.client.ImagePull(ctx, image, opts)
if err != nil {
return err
}
defer resp.Close()
reporter := newProgressReporter(image, cancel, d.imagePullProgressDeadline)
reporter.start()
defer reporter.stop()
decoder := json.NewDecoder(resp)
for {
var msg dockermessage.JSONMessage
err := decoder.Decode(&msg)
if err == io.EOF {
break
}
if err != nil {
return err
}
if msg.Error != nil {
return msg.Error
}
reporter.set(&msg)
}
return nil
}
func (d *kubeDockerClient) RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.ImageRemove(ctx, image, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if isImageNotFoundError(err) {
return nil, ImageNotFoundError{ID: image}
}
return resp, err
}
func (d *kubeDockerClient) Logs(id string, opts dockertypes.ContainerLogsOptions, sopts StreamOptions) error {
ctx, cancel := d.getCancelableContext()
defer cancel()
resp, err := d.client.ContainerLogs(ctx, id, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
if err != nil {
return err
}
defer resp.Close()
return d.redirectResponseToOutputStream(sopts.RawTerminal, sopts.OutputStream, sopts.ErrorStream, resp)
}
func (d *kubeDockerClient) Version() (*dockertypes.Version, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.ServerVersion(ctx)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *kubeDockerClient) Info() (*dockertypes.Info, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.Info(ctx)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &resp, nil
}
// TODO(random-liu): Add unit test for exec and attach functions, just like what go-dockerclient did.
func (d *kubeDockerClient) CreateExec(id string, opts dockertypes.ExecConfig) (*dockertypes.IDResponse, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.ContainerExecCreate(ctx, id, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *kubeDockerClient) StartExec(startExec string, opts dockertypes.ExecStartCheck, sopts StreamOptions) error {
ctx, cancel := d.getCancelableContext()
defer cancel()
if opts.Detach {
err := d.client.ContainerExecStart(ctx, startExec, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
return err
}
resp, err := d.client.ContainerExecAttach(ctx, startExec, dockertypes.ExecConfig{
Detach: opts.Detach,
Tty: opts.Tty,
})
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
if err != nil {
return err
}
defer resp.Close()
if sopts.ExecStarted != nil {
// Send a message to the channel indicating that the exec has started. This is needed so
// interactive execs can handle resizing correctly - the request to resize the TTY has to happen
// after the call to d.client.ContainerExecAttach, and because d.holdHijackedConnection below
// blocks, we use sopts.ExecStarted to signal the caller that it's ok to resize.
sopts.ExecStarted <- struct{}{}
}
return d.holdHijackedConnection(sopts.RawTerminal || opts.Tty, sopts.InputStream, sopts.OutputStream, sopts.ErrorStream, resp)
}
func (d *kubeDockerClient) InspectExec(id string) (*dockertypes.ContainerExecInspect, error) {
ctx, cancel := d.getTimeoutContext()
defer cancel()
resp, err := d.client.ContainerExecInspect(ctx, id)
if ctxErr := contextError(ctx); ctxErr != nil {
return nil, ctxErr
}
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *kubeDockerClient) AttachToContainer(id string, opts dockertypes.ContainerAttachOptions, sopts StreamOptions) error {
ctx, cancel := d.getCancelableContext()
defer cancel()
resp, err := d.client.ContainerAttach(ctx, id, opts)
if ctxErr := contextError(ctx); ctxErr != nil {
return ctxErr
}
if err != nil {
return err
}
defer resp.Close()
return d.holdHijackedConnection(sopts.RawTerminal, sopts.InputStream, sopts.OutputStream, sopts.ErrorStream, resp)
}
func (d *kubeDockerClient) ResizeExecTTY(id string, height, width uint) error {
ctx, cancel := d.getCancelableContext()
defer cancel()
return d.client.ContainerExecResize(ctx, id, dockertypes.ResizeOptions{
Height: height,
Width: width,
})
}
func (d *kubeDockerClient) ResizeContainerTTY(id string, height, width uint) error {
ctx, cancel := d.getCancelableContext()
defer cancel()
return d.client.ContainerResize(ctx, id, dockertypes.ResizeOptions{
Height: height,
Width: width,
})
}
// GetContainerStats is currently only used for Windows container stats
func (d *kubeDockerClient) GetContainerStats(id string) (*dockertypes.StatsJSON, error) {
ctx, cancel := d.getCancelableContext()
defer cancel()
response, err := d.client.ContainerStats(ctx, id, false)
if err != nil {
return nil, err
}
dec := json.NewDecoder(response.Body)
var stats dockertypes.StatsJSON
err = dec.Decode(&stats)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &stats, nil
}
// redirectResponseToOutputStream redirect the response stream to stdout and stderr. When tty is true, all stream will
// only be redirected to stdout.
func (d *kubeDockerClient) redirectResponseToOutputStream(tty bool, outputStream, errorStream io.Writer, resp io.Reader) error {
if outputStream == nil {
outputStream = ioutil.Discard
}
if errorStream == nil {
errorStream = ioutil.Discard
}
var err error
if tty {
_, err = io.Copy(outputStream, resp)
} else {
_, err = dockerstdcopy.StdCopy(outputStream, errorStream, resp)
}
return err
}
// holdHijackedConnection hold the HijackedResponse, redirect the inputStream to the connection, and redirect the response
// stream to stdout and stderr. NOTE: If needed, we could also add context in this function.
func (d *kubeDockerClient) holdHijackedConnection(tty bool, inputStream io.Reader, outputStream, errorStream io.Writer, resp dockertypes.HijackedResponse) error {
receiveStdout := make(chan error)
if outputStream != nil || errorStream != nil {
go func() {
receiveStdout <- d.redirectResponseToOutputStream(tty, outputStream, errorStream, resp.Reader)
}()
}
stdinDone := make(chan struct{})
go func() {
if inputStream != nil {
io.Copy(resp.Conn, inputStream)
}
resp.CloseWrite()
close(stdinDone)
}()
select {
case err := <-receiveStdout:
return err
case <-stdinDone:
if outputStream != nil || errorStream != nil {
return <-receiveStdout
}
}
return nil
}
// getCancelableContext returns a new cancelable context. For long running requests without timeout, we use cancelable
// context to avoid potential resource leak, although the current implementation shouldn't leak resource.
func (d *kubeDockerClient) getCancelableContext() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
}
// getTimeoutContext returns a new context with default request timeout
func (d *kubeDockerClient) getTimeoutContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), d.timeout)
}
// getCustomTimeoutContext returns a new context with a specific request timeout
func (d *kubeDockerClient) getCustomTimeoutContext(timeout time.Duration) (context.Context, context.CancelFunc) {
// Pick the larger of the two
if d.timeout > timeout {
timeout = d.timeout
}
return context.WithTimeout(context.Background(), timeout)
}
// contextError checks the context, and returns error if the context is timeout.
func contextError(ctx context.Context) error {
if ctx.Err() == context.DeadlineExceeded {
return operationTimeout{err: ctx.Err()}
}
return ctx.Err()
}
// StreamOptions are the options used to configure the stream redirection
type StreamOptions struct {
RawTerminal bool
InputStream io.Reader
OutputStream io.Writer
ErrorStream io.Writer
ExecStarted chan struct{}
}
// operationTimeout is the error returned when the docker operations are timeout.
type operationTimeout struct {
err error
}
func (e operationTimeout) Error() string {
return fmt.Sprintf("operation timeout: %v", e.err)
}
// containerNotFoundErrorRegx is the regexp of container not found error message.
var containerNotFoundErrorRegx = regexp.MustCompile(`No such container: [0-9a-z]+`)
// IsContainerNotFoundError checks whether the error is container not found error.
func IsContainerNotFoundError(err error) bool {
return containerNotFoundErrorRegx.MatchString(err.Error())
}
// ImageNotFoundError is the error returned by InspectImage when image not found.
// Expose this to inject error in dockershim for testing.
type ImageNotFoundError struct {
ID string
}
func (e ImageNotFoundError) Error() string {
return fmt.Sprintf("no such image: %q", e.ID)
}
// IsImageNotFoundError checks whether the error is image not found error. This is exposed
// to share with dockershim.
func IsImageNotFoundError(err error) bool {
_, ok := err.(ImageNotFoundError)
return ok
}

View File

@ -0,0 +1,33 @@
/*
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 libdocker
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsContainerNotFoundError(t *testing.T) {
// Expected error message from docker.
containerNotFoundError := fmt.Errorf("Error response from daemon: No such container: 96e914f31579e44fe49b239266385330a9b2125abeb9254badd9fca74580c95a")
otherError := fmt.Errorf("Error response from daemon: Other errors")
assert.True(t, IsContainerNotFoundError(containerNotFoundError))
assert.False(t, IsContainerNotFoundError(otherError))
}

View File

@ -0,0 +1,23 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["metrics.go"],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/metrics",
visibility = ["//visibility:public"],
deps = ["//vendor/github.com/prometheus/client_golang/prometheus:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,96 @@
/*
Copyright 2015 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 metrics
import (
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
const (
// DockerOperationsKey is the key for docker operation metrics.
DockerOperationsKey = "docker_operations"
// DockerOperationsLatencyKey is the key for the operation latency metrics.
DockerOperationsLatencyKey = "docker_operations_latency_microseconds"
// DockerOperationsErrorsKey is the key for the operation error metrics.
DockerOperationsErrorsKey = "docker_operations_errors"
// DockerOperationsTimeoutKey is the key for the operation timoeut metrics.
DockerOperationsTimeoutKey = "docker_operations_timeout"
// Keep the "kubelet" subsystem for backward compatibility.
kubeletSubsystem = "kubelet"
)
var (
// DockerOperationsLatency collects operation latency numbers by operation
// type.
DockerOperationsLatency = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Subsystem: kubeletSubsystem,
Name: DockerOperationsLatencyKey,
Help: "Latency in microseconds of Docker operations. Broken down by operation type.",
},
[]string{"operation_type"},
)
// DockerOperations collects operation counts by operation type.
DockerOperations = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: kubeletSubsystem,
Name: DockerOperationsKey,
Help: "Cumulative number of Docker operations by operation type.",
},
[]string{"operation_type"},
)
// DockerOperationsErrors collects operation errors by operation
// type.
DockerOperationsErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: kubeletSubsystem,
Name: DockerOperationsErrorsKey,
Help: "Cumulative number of Docker operation errors by operation type.",
},
[]string{"operation_type"},
)
// DockerOperationsTimeout collects operation timeouts by operation type.
DockerOperationsTimeout = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: kubeletSubsystem,
Name: DockerOperationsTimeoutKey,
Help: "Cumulative number of Docker operation timeout by operation type.",
},
[]string{"operation_type"},
)
)
var registerMetrics sync.Once
// Register all metrics.
func Register() {
registerMetrics.Do(func() {
prometheus.MustRegister(DockerOperationsLatency)
prometheus.MustRegister(DockerOperations)
prometheus.MustRegister(DockerOperationsErrors)
prometheus.MustRegister(DockerOperationsTimeout)
})
}
// SinceInMicroseconds gets the time since the specified start in microseconds.
func SinceInMicroseconds(start time.Time) float64 {
return float64(time.Since(start).Nanoseconds() / time.Microsecond.Nanoseconds())
}

View File

@ -0,0 +1,149 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"math/rand"
"strconv"
"strings"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/leaky"
)
// Container "names" are implementation details that do not concern
// kubelet/CRI. This CRI shim uses names to fulfill the CRI requirement to
// make sandbox/container creation idempotent. CRI states that there can
// only exist one sandbox/container with the given metadata. To enforce this,
// this shim constructs a name using the fields in the metadata so that
// docker will reject the creation request if the name already exists.
//
// Note that changes to naming will likely break the backward compatibility.
// Code must be added to ensure the shim knows how to recognize and extract
// information the older containers.
//
// TODO: Add code to handle backward compatibility, i.e., making sure we can
// recognize older containers and extract information from their names if
// necessary.
const (
// kubePrefix is used to identify the containers/sandboxes on the node managed by kubelet
kubePrefix = "k8s"
// sandboxContainerName is a string to include in the docker container so
// that users can easily identify the sandboxes.
sandboxContainerName = leaky.PodInfraContainerName
// Delimiter used to construct docker container names.
nameDelimiter = "_"
// DockerImageIDPrefix is the prefix of image id in container status.
DockerImageIDPrefix = "docker://"
// DockerPullableImageIDPrefix is the prefix of pullable image id in container status.
DockerPullableImageIDPrefix = "docker-pullable://"
)
func makeSandboxName(s *runtimeapi.PodSandboxConfig) string {
return strings.Join([]string{
kubePrefix, // 0
sandboxContainerName, // 1
s.Metadata.Name, // 2
s.Metadata.Namespace, // 3
s.Metadata.Uid, // 4
fmt.Sprintf("%d", s.Metadata.Attempt), // 5
}, nameDelimiter)
}
func makeContainerName(s *runtimeapi.PodSandboxConfig, c *runtimeapi.ContainerConfig) string {
return strings.Join([]string{
kubePrefix, // 0
c.Metadata.Name, // 1:
s.Metadata.Name, // 2: sandbox name
s.Metadata.Namespace, // 3: sandbox namesapce
s.Metadata.Uid, // 4 sandbox uid
fmt.Sprintf("%d", c.Metadata.Attempt), // 5
}, nameDelimiter)
}
// randomizeName randomizes the container name. This should only be used when we hit the
// docker container name conflict bug.
func randomizeName(name string) string {
return strings.Join([]string{
name,
fmt.Sprintf("%08x", rand.Uint32()),
}, nameDelimiter)
}
func parseUint32(s string) (uint32, error) {
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0, err
}
return uint32(n), nil
}
// TODO: Evaluate whether we should rely on labels completely.
func parseSandboxName(name string) (*runtimeapi.PodSandboxMetadata, error) {
// Docker adds a "/" prefix to names. so trim it.
name = strings.TrimPrefix(name, "/")
parts := strings.Split(name, nameDelimiter)
// Tolerate the random suffix.
// TODO(random-liu): Remove 7 field case when docker 1.11 is deprecated.
if len(parts) != 6 && len(parts) != 7 {
return nil, fmt.Errorf("failed to parse the sandbox name: %q", name)
}
if parts[0] != kubePrefix {
return nil, fmt.Errorf("container is not managed by kubernetes: %q", name)
}
attempt, err := parseUint32(parts[5])
if err != nil {
return nil, fmt.Errorf("failed to parse the sandbox name %q: %v", name, err)
}
return &runtimeapi.PodSandboxMetadata{
Name: parts[2],
Namespace: parts[3],
Uid: parts[4],
Attempt: attempt,
}, nil
}
// TODO: Evaluate whether we should rely on labels completely.
func parseContainerName(name string) (*runtimeapi.ContainerMetadata, error) {
// Docker adds a "/" prefix to names. so trim it.
name = strings.TrimPrefix(name, "/")
parts := strings.Split(name, nameDelimiter)
// Tolerate the random suffix.
// TODO(random-liu): Remove 7 field case when docker 1.11 is deprecated.
if len(parts) != 6 && len(parts) != 7 {
return nil, fmt.Errorf("failed to parse the container name: %q", name)
}
if parts[0] != kubePrefix {
return nil, fmt.Errorf("container is not managed by kubernetes: %q", name)
}
attempt, err := parseUint32(parts[5])
if err != nil {
return nil, fmt.Errorf("failed to parse the container name %q: %v", name, err)
}
return &runtimeapi.ContainerMetadata{
Name: parts[1],
Attempt: attempt,
}, nil
}

View File

@ -0,0 +1,106 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"testing"
"github.com/stretchr/testify/assert"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func TestSandboxNameRoundTrip(t *testing.T) {
config := makeSandboxConfig("foo", "bar", "iamuid", 3)
actualName := makeSandboxName(config)
assert.Equal(t, "k8s_POD_foo_bar_iamuid_3", actualName)
actualMetadata, err := parseSandboxName(actualName)
assert.NoError(t, err)
assert.Equal(t, config.Metadata, actualMetadata)
}
func TestNonParsableSandboxNames(t *testing.T) {
// All names must start with the kubernetes prefix "k8s".
_, err := parseSandboxName("owner_POD_foo_bar_iamuid_4")
assert.Error(t, err)
// All names must contain exactly 6 parts.
_, err = parseSandboxName("k8s_POD_dummy_foo_bar_iamuid_4")
assert.Error(t, err)
_, err = parseSandboxName("k8s_foo_bar_iamuid_4")
assert.Error(t, err)
// Should be able to parse attempt number.
_, err = parseSandboxName("k8s_POD_foo_bar_iamuid_notanumber")
assert.Error(t, err)
}
func TestContainerNameRoundTrip(t *testing.T) {
sConfig := makeSandboxConfig("foo", "bar", "iamuid", 3)
name, attempt := "pause", uint32(5)
config := &runtimeapi.ContainerConfig{
Metadata: &runtimeapi.ContainerMetadata{
Name: name,
Attempt: attempt,
},
}
actualName := makeContainerName(sConfig, config)
assert.Equal(t, "k8s_pause_foo_bar_iamuid_5", actualName)
actualMetadata, err := parseContainerName(actualName)
assert.NoError(t, err)
assert.Equal(t, config.Metadata, actualMetadata)
}
func TestNonParsableContainerNames(t *testing.T) {
// All names must start with the kubernetes prefix "k8s".
_, err := parseContainerName("owner_frontend_foo_bar_iamuid_4")
assert.Error(t, err)
// All names must contain exactly 6 parts.
_, err = parseContainerName("k8s_frontend_dummy_foo_bar_iamuid_4")
assert.Error(t, err)
_, err = parseContainerName("k8s_foo_bar_iamuid_4")
assert.Error(t, err)
// Should be able to parse attempt number.
_, err = parseContainerName("k8s_frontend_foo_bar_iamuid_notanumber")
assert.Error(t, err)
}
func TestParseRandomizedNames(t *testing.T) {
// Test randomized sandbox name.
sConfig := makeSandboxConfig("foo", "bar", "iamuid", 3)
sActualName := randomizeName(makeSandboxName(sConfig))
sActualMetadata, err := parseSandboxName(sActualName)
assert.NoError(t, err)
assert.Equal(t, sConfig.Metadata, sActualMetadata)
// Test randomized container name.
name, attempt := "pause", uint32(5)
config := &runtimeapi.ContainerConfig{
Metadata: &runtimeapi.ContainerMetadata{
Name: name,
Attempt: attempt,
},
}
actualName := randomizeName(makeContainerName(sConfig, config))
actualMetadata, err := parseContainerName(actualName)
assert.NoError(t, err)
assert.Equal(t, config.Metadata, actualMetadata)
}

View File

@ -0,0 +1,39 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"docker_server.go",
"docker_service.go",
],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/remote",
deps = [
"//pkg/kubelet/apis/cri:go_default_library",
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
"//pkg/kubelet/dockershim:go_default_library",
"//pkg/kubelet/util:go_default_library",
"//pkg/util/interrupt:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/google.golang.org/grpc:go_default_library",
"//vendor/k8s.io/utils/exec:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,75 @@
/*
Copyright 2016 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 remote
import (
"fmt"
"github.com/golang/glog"
"google.golang.org/grpc"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim"
"k8s.io/kubernetes/pkg/kubelet/util"
"k8s.io/kubernetes/pkg/util/interrupt"
)
// DockerServer is the grpc server of dockershim.
type DockerServer struct {
// endpoint is the endpoint to serve on.
endpoint string
// service is the docker service which implements runtime and image services.
service DockerService
// server is the grpc server.
server *grpc.Server
}
// NewDockerServer creates the dockershim grpc server.
func NewDockerServer(endpoint string, s dockershim.DockerService) *DockerServer {
return &DockerServer{
endpoint: endpoint,
service: NewDockerService(s),
}
}
// Start starts the dockershim grpc server.
func (s *DockerServer) Start() error {
glog.V(2).Infof("Start dockershim grpc server")
l, err := util.CreateListener(s.endpoint)
if err != nil {
return fmt.Errorf("failed to listen on %q: %v", s.endpoint, err)
}
// Create the grpc server and register runtime and image services.
s.server = grpc.NewServer()
runtimeapi.RegisterRuntimeServiceServer(s.server, s.service)
runtimeapi.RegisterImageServiceServer(s.server, s.service)
go func() {
// Use interrupt handler to make sure the server to be stopped properly.
h := interrupt.New(nil, s.Stop)
err := h.Run(func() error { return s.server.Serve(l) })
if err != nil {
glog.Errorf("Failed to serve connections: %v", err)
}
}()
return nil
}
// Stop stops the dockershim grpc server.
func (s *DockerServer) Stop() {
glog.V(2).Infof("Stop docker server")
s.server.Stop()
}

View File

@ -0,0 +1,249 @@
/*
Copyright 2016 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 remote
import (
"time"
"golang.org/x/net/context"
internalapi "k8s.io/kubernetes/pkg/kubelet/apis/cri"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockershim"
utilexec "k8s.io/utils/exec"
)
// DockerService is the interface implement CRI remote service server.
type DockerService interface {
runtimeapi.RuntimeServiceServer
runtimeapi.ImageServiceServer
}
// dockerService uses dockershim service to implement DockerService.
// Notice that the contexts in the functions are not used now.
// TODO(random-liu): Change the dockershim service to support context, and implement
// internal services and remote services with the dockershim service.
type dockerService struct {
runtimeService internalapi.RuntimeService
imageService internalapi.ImageManagerService
}
func NewDockerService(s dockershim.DockerService) DockerService {
return &dockerService{runtimeService: s, imageService: s}
}
func (d *dockerService) Version(ctx context.Context, r *runtimeapi.VersionRequest) (*runtimeapi.VersionResponse, error) {
return d.runtimeService.Version(r.Version)
}
func (d *dockerService) Status(ctx context.Context, r *runtimeapi.StatusRequest) (*runtimeapi.StatusResponse, error) {
status, err := d.runtimeService.Status()
if err != nil {
return nil, err
}
return &runtimeapi.StatusResponse{Status: status}, nil
}
func (d *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
podSandboxId, err := d.runtimeService.RunPodSandbox(r.GetConfig())
if err != nil {
return nil, err
}
return &runtimeapi.RunPodSandboxResponse{PodSandboxId: podSandboxId}, nil
}
func (d *dockerService) StopPodSandbox(ctx context.Context, r *runtimeapi.StopPodSandboxRequest) (*runtimeapi.StopPodSandboxResponse, error) {
err := d.runtimeService.StopPodSandbox(r.PodSandboxId)
if err != nil {
return nil, err
}
return &runtimeapi.StopPodSandboxResponse{}, nil
}
func (d *dockerService) RemovePodSandbox(ctx context.Context, r *runtimeapi.RemovePodSandboxRequest) (*runtimeapi.RemovePodSandboxResponse, error) {
err := d.runtimeService.RemovePodSandbox(r.PodSandboxId)
if err != nil {
return nil, err
}
return &runtimeapi.RemovePodSandboxResponse{}, nil
}
func (d *dockerService) PodSandboxStatus(ctx context.Context, r *runtimeapi.PodSandboxStatusRequest) (*runtimeapi.PodSandboxStatusResponse, error) {
podSandboxStatus, err := d.runtimeService.PodSandboxStatus(r.PodSandboxId)
if err != nil {
return nil, err
}
return &runtimeapi.PodSandboxStatusResponse{Status: podSandboxStatus}, nil
}
func (d *dockerService) ListPodSandbox(ctx context.Context, r *runtimeapi.ListPodSandboxRequest) (*runtimeapi.ListPodSandboxResponse, error) {
items, err := d.runtimeService.ListPodSandbox(r.GetFilter())
if err != nil {
return nil, err
}
return &runtimeapi.ListPodSandboxResponse{Items: items}, nil
}
func (d *dockerService) CreateContainer(ctx context.Context, r *runtimeapi.CreateContainerRequest) (*runtimeapi.CreateContainerResponse, error) {
containerId, err := d.runtimeService.CreateContainer(r.PodSandboxId, r.GetConfig(), r.GetSandboxConfig())
if err != nil {
return nil, err
}
return &runtimeapi.CreateContainerResponse{ContainerId: containerId}, nil
}
func (d *dockerService) StartContainer(ctx context.Context, r *runtimeapi.StartContainerRequest) (*runtimeapi.StartContainerResponse, error) {
err := d.runtimeService.StartContainer(r.ContainerId)
if err != nil {
return nil, err
}
return &runtimeapi.StartContainerResponse{}, nil
}
func (d *dockerService) StopContainer(ctx context.Context, r *runtimeapi.StopContainerRequest) (*runtimeapi.StopContainerResponse, error) {
err := d.runtimeService.StopContainer(r.ContainerId, r.Timeout)
if err != nil {
return nil, err
}
return &runtimeapi.StopContainerResponse{}, nil
}
func (d *dockerService) RemoveContainer(ctx context.Context, r *runtimeapi.RemoveContainerRequest) (*runtimeapi.RemoveContainerResponse, error) {
err := d.runtimeService.RemoveContainer(r.ContainerId)
if err != nil {
return nil, err
}
return &runtimeapi.RemoveContainerResponse{}, nil
}
func (d *dockerService) ListContainers(ctx context.Context, r *runtimeapi.ListContainersRequest) (*runtimeapi.ListContainersResponse, error) {
containers, err := d.runtimeService.ListContainers(r.GetFilter())
if err != nil {
return nil, err
}
return &runtimeapi.ListContainersResponse{Containers: containers}, nil
}
func (d *dockerService) ContainerStatus(ctx context.Context, r *runtimeapi.ContainerStatusRequest) (*runtimeapi.ContainerStatusResponse, error) {
status, err := d.runtimeService.ContainerStatus(r.ContainerId)
if err != nil {
return nil, err
}
return &runtimeapi.ContainerStatusResponse{Status: status}, nil
}
func (d *dockerService) UpdateContainerResources(ctx context.Context, r *runtimeapi.UpdateContainerResourcesRequest) (*runtimeapi.UpdateContainerResourcesResponse, error) {
err := d.runtimeService.UpdateContainerResources(r.ContainerId, r.Linux)
if err != nil {
return nil, err
}
return &runtimeapi.UpdateContainerResourcesResponse{}, nil
}
func (d *dockerService) ExecSync(ctx context.Context, r *runtimeapi.ExecSyncRequest) (*runtimeapi.ExecSyncResponse, error) {
stdout, stderr, err := d.runtimeService.ExecSync(r.ContainerId, r.Cmd, time.Duration(r.Timeout)*time.Second)
var exitCode int32
if err != nil {
exitError, ok := err.(utilexec.ExitError)
if !ok {
return nil, err
}
exitCode = int32(exitError.ExitStatus())
}
return &runtimeapi.ExecSyncResponse{
Stdout: stdout,
Stderr: stderr,
ExitCode: exitCode,
}, nil
}
func (d *dockerService) Exec(ctx context.Context, r *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
return d.runtimeService.Exec(r)
}
func (d *dockerService) Attach(ctx context.Context, r *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) {
return d.runtimeService.Attach(r)
}
func (d *dockerService) PortForward(ctx context.Context, r *runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error) {
return d.runtimeService.PortForward(r)
}
func (d *dockerService) UpdateRuntimeConfig(ctx context.Context, r *runtimeapi.UpdateRuntimeConfigRequest) (*runtimeapi.UpdateRuntimeConfigResponse, error) {
err := d.runtimeService.UpdateRuntimeConfig(r.GetRuntimeConfig())
if err != nil {
return nil, err
}
return &runtimeapi.UpdateRuntimeConfigResponse{}, nil
}
func (d *dockerService) ListImages(ctx context.Context, r *runtimeapi.ListImagesRequest) (*runtimeapi.ListImagesResponse, error) {
images, err := d.imageService.ListImages(r.GetFilter())
if err != nil {
return nil, err
}
return &runtimeapi.ListImagesResponse{Images: images}, nil
}
func (d *dockerService) ImageStatus(ctx context.Context, r *runtimeapi.ImageStatusRequest) (*runtimeapi.ImageStatusResponse, error) {
image, err := d.imageService.ImageStatus(r.GetImage())
if err != nil {
return nil, err
}
return &runtimeapi.ImageStatusResponse{Image: image}, nil
}
func (d *dockerService) PullImage(ctx context.Context, r *runtimeapi.PullImageRequest) (*runtimeapi.PullImageResponse, error) {
image, err := d.imageService.PullImage(r.GetImage(), r.GetAuth())
if err != nil {
return nil, err
}
return &runtimeapi.PullImageResponse{ImageRef: image}, nil
}
func (d *dockerService) RemoveImage(ctx context.Context, r *runtimeapi.RemoveImageRequest) (*runtimeapi.RemoveImageResponse, error) {
err := d.imageService.RemoveImage(r.GetImage())
if err != nil {
return nil, err
}
return &runtimeapi.RemoveImageResponse{}, nil
}
// ImageFsInfo returns information of the filesystem that is used to store images.
func (d *dockerService) ImageFsInfo(ctx context.Context, r *runtimeapi.ImageFsInfoRequest) (*runtimeapi.ImageFsInfoResponse, error) {
filesystems, err := d.imageService.ImageFsInfo()
if err != nil {
return nil, err
}
return &runtimeapi.ImageFsInfoResponse{ImageFilesystems: filesystems}, nil
}
func (d *dockerService) ContainerStats(ctx context.Context, r *runtimeapi.ContainerStatsRequest) (*runtimeapi.ContainerStatsResponse, error) {
stats, err := d.runtimeService.ContainerStats(r.ContainerId)
if err != nil {
return nil, err
}
return &runtimeapi.ContainerStatsResponse{Stats: stats}, nil
}
func (d *dockerService) ListContainerStats(ctx context.Context, r *runtimeapi.ListContainerStatsRequest) (*runtimeapi.ListContainerStatsResponse, error) {
stats, err := d.runtimeService.ListContainerStats(r.GetFilter())
if err != nil {
return nil, err
}
return &runtimeapi.ListContainerStatsResponse{Stats: stats}, nil
}

View File

@ -0,0 +1,204 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"strconv"
"strings"
"github.com/blang/semver"
dockercontainer "github.com/docker/docker/api/types/container"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
knetwork "k8s.io/kubernetes/pkg/kubelet/network"
)
// applySandboxSecurityContext updates docker sandbox options according to security context.
func applySandboxSecurityContext(lc *runtimeapi.LinuxPodSandboxConfig, config *dockercontainer.Config, hc *dockercontainer.HostConfig, network *knetwork.PluginManager, separator rune) error {
if lc == nil {
return nil
}
var sc *runtimeapi.LinuxContainerSecurityContext
if lc.SecurityContext != nil {
sc = &runtimeapi.LinuxContainerSecurityContext{
SupplementalGroups: lc.SecurityContext.SupplementalGroups,
RunAsUser: lc.SecurityContext.RunAsUser,
ReadonlyRootfs: lc.SecurityContext.ReadonlyRootfs,
SelinuxOptions: lc.SecurityContext.SelinuxOptions,
NamespaceOptions: lc.SecurityContext.NamespaceOptions,
}
}
modifyContainerConfig(sc, config)
if err := modifyHostConfig(sc, hc, separator); err != nil {
return err
}
modifySandboxNamespaceOptions(sc.GetNamespaceOptions(), hc, network)
return nil
}
// applyContainerSecurityContext updates docker container options according to security context.
func applyContainerSecurityContext(lc *runtimeapi.LinuxContainerConfig, podSandboxID string, config *dockercontainer.Config, hc *dockercontainer.HostConfig, separator rune) error {
if lc == nil {
return nil
}
modifyContainerConfig(lc.SecurityContext, config)
if err := modifyHostConfig(lc.SecurityContext, hc, separator); err != nil {
return err
}
modifyContainerNamespaceOptions(lc.SecurityContext.GetNamespaceOptions(), podSandboxID, hc)
return nil
}
// modifyContainerConfig applies container security context config to dockercontainer.Config.
func modifyContainerConfig(sc *runtimeapi.LinuxContainerSecurityContext, config *dockercontainer.Config) {
if sc == nil {
return
}
if sc.RunAsUser != nil {
config.User = strconv.FormatInt(sc.GetRunAsUser().Value, 10)
}
if sc.RunAsUsername != "" {
config.User = sc.RunAsUsername
}
}
// modifyHostConfig applies security context config to dockercontainer.HostConfig.
func modifyHostConfig(sc *runtimeapi.LinuxContainerSecurityContext, hostConfig *dockercontainer.HostConfig, separator rune) error {
if sc == nil {
return nil
}
// Apply supplemental groups.
for _, group := range sc.SupplementalGroups {
hostConfig.GroupAdd = append(hostConfig.GroupAdd, strconv.FormatInt(group, 10))
}
// Apply security context for the container.
hostConfig.Privileged = sc.Privileged
hostConfig.ReadonlyRootfs = sc.ReadonlyRootfs
if sc.Capabilities != nil {
hostConfig.CapAdd = sc.GetCapabilities().AddCapabilities
hostConfig.CapDrop = sc.GetCapabilities().DropCapabilities
}
if sc.SelinuxOptions != nil {
hostConfig.SecurityOpt = addSELinuxOptions(
hostConfig.SecurityOpt,
sc.SelinuxOptions,
separator,
)
}
// Apply apparmor options.
apparmorSecurityOpts, err := getApparmorSecurityOpts(sc, separator)
if err != nil {
return fmt.Errorf("failed to generate apparmor security options: %v", err)
}
hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, apparmorSecurityOpts...)
if sc.NoNewPrivs {
hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, "no-new-privileges")
}
return nil
}
// modifySandboxNamespaceOptions apply namespace options for sandbox
func modifySandboxNamespaceOptions(nsOpts *runtimeapi.NamespaceOption, hostConfig *dockercontainer.HostConfig, network *knetwork.PluginManager) {
hostNetwork := false
if nsOpts != nil {
hostNetwork = nsOpts.HostNetwork
}
modifyCommonNamespaceOptions(nsOpts, hostConfig)
modifyHostNetworkOptionForSandbox(hostNetwork, network, hostConfig)
}
// modifyContainerNamespaceOptions apply namespace options for container
func modifyContainerNamespaceOptions(nsOpts *runtimeapi.NamespaceOption, podSandboxID string, hostConfig *dockercontainer.HostConfig) {
hostNetwork := false
if nsOpts != nil {
hostNetwork = nsOpts.HostNetwork
}
hostConfig.PidMode = dockercontainer.PidMode(fmt.Sprintf("container:%v", podSandboxID))
modifyCommonNamespaceOptions(nsOpts, hostConfig)
modifyHostNetworkOptionForContainer(hostNetwork, podSandboxID, hostConfig)
}
// modifyCommonNamespaceOptions apply common namespace options for sandbox and container
func modifyCommonNamespaceOptions(nsOpts *runtimeapi.NamespaceOption, hostConfig *dockercontainer.HostConfig) {
if nsOpts != nil {
if nsOpts.HostPid {
hostConfig.PidMode = namespaceModeHost
}
if nsOpts.HostIpc {
hostConfig.IpcMode = namespaceModeHost
}
}
}
// modifyHostNetworkOptionForSandbox applies NetworkMode/UTSMode to sandbox's dockercontainer.HostConfig.
func modifyHostNetworkOptionForSandbox(hostNetwork bool, network *knetwork.PluginManager, hc *dockercontainer.HostConfig) {
if hostNetwork {
hc.NetworkMode = namespaceModeHost
return
}
if network == nil {
hc.NetworkMode = "default"
return
}
switch network.PluginName() {
case "cni":
fallthrough
case "kubenet":
hc.NetworkMode = "none"
default:
hc.NetworkMode = "default"
}
}
// modifyHostNetworkOptionForContainer applies NetworkMode/UTSMode to container's dockercontainer.HostConfig.
func modifyHostNetworkOptionForContainer(hostNetwork bool, podSandboxID string, hc *dockercontainer.HostConfig) {
sandboxNSMode := fmt.Sprintf("container:%v", podSandboxID)
hc.NetworkMode = dockercontainer.NetworkMode(sandboxNSMode)
hc.IpcMode = dockercontainer.IpcMode(sandboxNSMode)
hc.UTSMode = ""
if hostNetwork {
hc.UTSMode = namespaceModeHost
}
}
// modifyPIDNamespaceOverrides implements two temporary overrides for the default PID namespace sharing for Docker:
// 1. Docker engine prior to API Version 1.24 doesn't support attaching to another container's
// PID namespace, and it didn't stabilize until 1.26. This check can be removed when Kubernetes'
// minimum Docker version is at least 1.13.1 (API version 1.26).
// 2. The administrator has overridden the default behavior by means of a kubelet flag. This is an
// "escape hatch" to return to previous behavior of isolated namespaces and should be removed once
// no longer needed.
func modifyPIDNamespaceOverrides(disableSharedPID bool, version *semver.Version, hc *dockercontainer.HostConfig) {
if !strings.HasPrefix(string(hc.PidMode), "container:") {
return
}
if disableSharedPID || version.LT(semver.Version{Major: 1, Minor: 26}) {
hc.PidMode = ""
}
}

View File

@ -0,0 +1,423 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"fmt"
"strconv"
"testing"
"github.com/blang/semver"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
func TestModifyContainerConfig(t *testing.T) {
var uid int64 = 123
var username = "testuser"
cases := []struct {
name string
sc *runtimeapi.LinuxContainerSecurityContext
expected *dockercontainer.Config
}{
{
name: "container.SecurityContext.RunAsUser set",
sc: &runtimeapi.LinuxContainerSecurityContext{
RunAsUser: &runtimeapi.Int64Value{Value: uid},
},
expected: &dockercontainer.Config{
User: strconv.FormatInt(uid, 10),
},
},
{
name: "container.SecurityContext.RunAsUsername set",
sc: &runtimeapi.LinuxContainerSecurityContext{
RunAsUsername: username,
},
expected: &dockercontainer.Config{
User: username,
},
},
{
name: "no RunAsUser value set",
sc: &runtimeapi.LinuxContainerSecurityContext{},
expected: &dockercontainer.Config{},
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.Config{}
modifyContainerConfig(tc.sc, dockerCfg)
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifyHostConfig(t *testing.T) {
setNetworkHC := &dockercontainer.HostConfig{}
setPrivSC := &runtimeapi.LinuxContainerSecurityContext{}
setPrivSC.Privileged = true
setPrivHC := &dockercontainer.HostConfig{
Privileged: true,
}
setCapsHC := &dockercontainer.HostConfig{
CapAdd: []string{"addCapA", "addCapB"},
CapDrop: []string{"dropCapA", "dropCapB"},
}
setSELinuxHC := &dockercontainer.HostConfig{
SecurityOpt: []string{
fmt.Sprintf("%s:%s", selinuxLabelUser('='), "user"),
fmt.Sprintf("%s:%s", selinuxLabelRole('='), "role"),
fmt.Sprintf("%s:%s", selinuxLabelType('='), "type"),
fmt.Sprintf("%s:%s", selinuxLabelLevel('='), "level"),
},
}
cases := []struct {
name string
sc *runtimeapi.LinuxContainerSecurityContext
expected *dockercontainer.HostConfig
}{
{
name: "fully set container.SecurityContext",
sc: fullValidSecurityContext(),
expected: fullValidHostConfig(),
},
{
name: "empty container.SecurityContext",
sc: &runtimeapi.LinuxContainerSecurityContext{},
expected: setNetworkHC,
},
{
name: "container.SecurityContext.Privileged",
sc: setPrivSC,
expected: setPrivHC,
},
{
name: "container.SecurityContext.Capabilities",
sc: &runtimeapi.LinuxContainerSecurityContext{
Capabilities: inputCapabilities(),
},
expected: setCapsHC,
},
{
name: "container.SecurityContext.SELinuxOptions",
sc: &runtimeapi.LinuxContainerSecurityContext{
SelinuxOptions: inputSELinuxOptions(),
},
expected: setSELinuxHC,
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.HostConfig{}
modifyHostConfig(tc.sc, dockerCfg, '=')
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifyHostConfigWithGroups(t *testing.T) {
supplementalGroupsSC := &runtimeapi.LinuxContainerSecurityContext{}
supplementalGroupsSC.SupplementalGroups = []int64{2222}
supplementalGroupHC := &dockercontainer.HostConfig{}
supplementalGroupHC.GroupAdd = []string{"2222"}
testCases := []struct {
name string
securityContext *runtimeapi.LinuxContainerSecurityContext
expected *dockercontainer.HostConfig
}{
{
name: "nil",
securityContext: nil,
expected: &dockercontainer.HostConfig{},
},
{
name: "SupplementalGroup",
securityContext: supplementalGroupsSC,
expected: supplementalGroupHC,
},
}
for _, tc := range testCases {
dockerCfg := &dockercontainer.HostConfig{}
modifyHostConfig(tc.securityContext, dockerCfg, '=')
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifyHostConfigAndNamespaceOptionsForContainer(t *testing.T) {
priv := true
sandboxID := "sandbox"
sandboxNSMode := fmt.Sprintf("container:%v", sandboxID)
setPrivSC := &runtimeapi.LinuxContainerSecurityContext{}
setPrivSC.Privileged = priv
setPrivHC := &dockercontainer.HostConfig{
Privileged: true,
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
PidMode: dockercontainer.PidMode(sandboxNSMode),
}
setCapsHC := &dockercontainer.HostConfig{
CapAdd: []string{"addCapA", "addCapB"},
CapDrop: []string{"dropCapA", "dropCapB"},
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
PidMode: dockercontainer.PidMode(sandboxNSMode),
}
setSELinuxHC := &dockercontainer.HostConfig{
SecurityOpt: []string{
fmt.Sprintf("%s:%s", selinuxLabelUser('='), "user"),
fmt.Sprintf("%s:%s", selinuxLabelRole('='), "role"),
fmt.Sprintf("%s:%s", selinuxLabelType('='), "type"),
fmt.Sprintf("%s:%s", selinuxLabelLevel('='), "level"),
},
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
PidMode: dockercontainer.PidMode(sandboxNSMode),
}
cases := []struct {
name string
sc *runtimeapi.LinuxContainerSecurityContext
expected *dockercontainer.HostConfig
}{
{
name: "container.SecurityContext.Privileged",
sc: setPrivSC,
expected: setPrivHC,
},
{
name: "container.SecurityContext.Capabilities",
sc: &runtimeapi.LinuxContainerSecurityContext{
Capabilities: inputCapabilities(),
},
expected: setCapsHC,
},
{
name: "container.SecurityContext.SELinuxOptions",
sc: &runtimeapi.LinuxContainerSecurityContext{
SelinuxOptions: inputSELinuxOptions(),
},
expected: setSELinuxHC,
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.HostConfig{}
modifyHostConfig(tc.sc, dockerCfg, '=')
modifyContainerNamespaceOptions(tc.sc.GetNamespaceOptions(), sandboxID, dockerCfg)
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifySandboxNamespaceOptions(t *testing.T) {
set := true
cases := []struct {
name string
nsOpt *runtimeapi.NamespaceOption
expected *dockercontainer.HostConfig
}{
{
name: "NamespaceOption.HostNetwork",
nsOpt: &runtimeapi.NamespaceOption{
HostNetwork: set,
},
expected: &dockercontainer.HostConfig{
NetworkMode: namespaceModeHost,
},
},
{
name: "NamespaceOption.HostIpc",
nsOpt: &runtimeapi.NamespaceOption{
HostIpc: set,
},
expected: &dockercontainer.HostConfig{
IpcMode: namespaceModeHost,
NetworkMode: "default",
},
},
{
name: "NamespaceOption.HostPid",
nsOpt: &runtimeapi.NamespaceOption{
HostPid: set,
},
expected: &dockercontainer.HostConfig{
PidMode: namespaceModeHost,
NetworkMode: "default",
},
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.HostConfig{}
modifySandboxNamespaceOptions(tc.nsOpt, dockerCfg, nil)
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifyContainerNamespaceOptions(t *testing.T) {
set := true
sandboxID := "sandbox"
sandboxNSMode := fmt.Sprintf("container:%v", sandboxID)
cases := []struct {
name string
nsOpt *runtimeapi.NamespaceOption
expected *dockercontainer.HostConfig
}{
{
name: "NamespaceOption.HostNetwork",
nsOpt: &runtimeapi.NamespaceOption{
HostNetwork: set,
},
expected: &dockercontainer.HostConfig{
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
UTSMode: namespaceModeHost,
PidMode: dockercontainer.PidMode(sandboxNSMode),
},
},
{
name: "NamespaceOption.HostIpc",
nsOpt: &runtimeapi.NamespaceOption{
HostIpc: set,
},
expected: &dockercontainer.HostConfig{
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
PidMode: dockercontainer.PidMode(sandboxNSMode),
},
},
{
name: "NamespaceOption.HostPid",
nsOpt: &runtimeapi.NamespaceOption{
HostPid: set,
},
expected: &dockercontainer.HostConfig{
NetworkMode: dockercontainer.NetworkMode(sandboxNSMode),
IpcMode: dockercontainer.IpcMode(sandboxNSMode),
PidMode: namespaceModeHost,
},
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.HostConfig{}
modifyContainerNamespaceOptions(tc.nsOpt, sandboxID, dockerCfg)
assert.Equal(t, tc.expected, dockerCfg, "[Test case %q]", tc.name)
}
}
func TestModifyContainerNamespacePIDOverride(t *testing.T) {
cases := []struct {
name string
disable bool
version *semver.Version
input, expected dockercontainer.PidMode
}{
{
name: "SharedPID.Enable",
disable: false,
version: &semver.Version{Major: 1, Minor: 26},
input: "container:sandbox",
expected: "container:sandbox",
},
{
name: "SharedPID.Disable",
disable: true,
version: &semver.Version{Major: 1, Minor: 26},
input: "container:sandbox",
expected: "",
},
{
name: "SharedPID.OldDocker",
disable: false,
version: &semver.Version{Major: 1, Minor: 25},
input: "container:sandbox",
expected: "",
},
{
name: "SharedPID.HostPid",
disable: true,
version: &semver.Version{Major: 1, Minor: 27},
input: "host",
expected: "host",
},
{
name: "SharedPID.DistantFuture",
disable: false,
version: &semver.Version{Major: 2, Minor: 10},
input: "container:sandbox",
expected: "container:sandbox",
},
{
name: "SharedPID.EmptyPidMode",
disable: true,
version: &semver.Version{Major: 1, Minor: 25},
input: "",
expected: "",
},
}
for _, tc := range cases {
dockerCfg := &dockercontainer.HostConfig{PidMode: tc.input}
modifyPIDNamespaceOverrides(tc.disable, tc.version, dockerCfg)
assert.Equal(t, tc.expected, dockerCfg.PidMode, "[Test case %q]", tc.name)
}
}
func fullValidSecurityContext() *runtimeapi.LinuxContainerSecurityContext {
return &runtimeapi.LinuxContainerSecurityContext{
Privileged: true,
Capabilities: inputCapabilities(),
SelinuxOptions: inputSELinuxOptions(),
}
}
func inputCapabilities() *runtimeapi.Capability {
return &runtimeapi.Capability{
AddCapabilities: []string{"addCapA", "addCapB"},
DropCapabilities: []string{"dropCapA", "dropCapB"},
}
}
func inputSELinuxOptions() *runtimeapi.SELinuxOption {
user := "user"
role := "role"
stype := "type"
level := "level"
return &runtimeapi.SELinuxOption{
User: user,
Role: role,
Type: stype,
Level: level,
}
}
func fullValidHostConfig() *dockercontainer.HostConfig {
return &dockercontainer.HostConfig{
Privileged: true,
CapAdd: []string{"addCapA", "addCapB"},
CapDrop: []string{"dropCapA", "dropCapB"},
SecurityOpt: []string{
fmt.Sprintf("%s:%s", selinuxLabelUser('='), "user"),
fmt.Sprintf("%s:%s", selinuxLabelRole('='), "role"),
fmt.Sprintf("%s:%s", selinuxLabelType('='), "type"),
fmt.Sprintf("%s:%s", selinuxLabelLevel('='), "level"),
},
}
}

View File

@ -0,0 +1,92 @@
/*
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 dockershim
import (
"fmt"
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
)
// selinuxLabelUser returns the fragment of a Docker security opt that
// describes the SELinux user. Note that strictly speaking this is not
// actually the name of the security opt, but a fragment of the whole key-
// value pair necessary to set the opt.
func selinuxLabelUser(separator rune) string {
return fmt.Sprintf("label%cuser", separator)
}
// selinuxLabelRole returns the fragment of a Docker security opt that
// describes the SELinux role. Note that strictly speaking this is not
// actually the name of the security opt, but a fragment of the whole key-
// value pair necessary to set the opt.
func selinuxLabelRole(separator rune) string {
return fmt.Sprintf("label%crole", separator)
}
// selinuxLabelType returns the fragment of a Docker security opt that
// describes the SELinux type. Note that strictly speaking this is not
// actually the name of the security opt, but a fragment of the whole key-
// value pair necessary to set the opt.
func selinuxLabelType(separator rune) string {
return fmt.Sprintf("label%ctype", separator)
}
// selinuxLabelLevel returns the fragment of a Docker security opt that
// describes the SELinux level. Note that strictly speaking this is not
// actually the name of the security opt, but a fragment of the whole key-
// value pair necessary to set the opt.
func selinuxLabelLevel(separator rune) string {
return fmt.Sprintf("label%clevel", separator)
}
// dockerLaelDisable returns the Docker security opt that disables SELinux for
// the container.
func selinuxLabelDisable(separator rune) string {
return fmt.Sprintf("label%cdisable", separator)
}
// addSELinuxOptions adds SELinux options to config using the given
// separator.
func addSELinuxOptions(config []string, selinuxOpts *runtimeapi.SELinuxOption, separator rune) []string {
// Note, strictly speaking, we are actually mutating the values of these
// keys, rather than formatting name and value into a string. Docker re-
// uses the same option name multiple times (it's just 'label') with
// different values which are themselves key-value pairs. For example,
// the SELinux type is represented by the security opt:
//
// label<separator>type:<selinux_type>
//
// In Docker API versions before 1.23, the separator was the `:` rune; in
// API version 1.23 it changed to the `=` rune.
config = modifySecurityOption(config, selinuxLabelUser(separator), selinuxOpts.User)
config = modifySecurityOption(config, selinuxLabelRole(separator), selinuxOpts.Role)
config = modifySecurityOption(config, selinuxLabelType(separator), selinuxOpts.Type)
config = modifySecurityOption(config, selinuxLabelLevel(separator), selinuxOpts.Level)
return config
}
// modifySecurityOption adds the security option of name to the config array
// with value in the form of name:value.
func modifySecurityOption(config []string, name, value string) []string {
if len(value) > 0 {
config = append(config, fmt.Sprintf("%s:%s", name, value))
}
return config
}

View File

@ -0,0 +1,54 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"reflect"
"testing"
)
func TestModifySecurityOptions(t *testing.T) {
testCases := []struct {
name string
config []string
optName string
optVal string
expected []string
}{
{
name: "Empty val",
config: []string{"a:b", "c:d"},
optName: "optA",
optVal: "",
expected: []string{"a:b", "c:d"},
},
{
name: "Valid",
config: []string{"a:b", "c:d"},
optName: "e",
optVal: "f",
expected: []string{"a:b", "c:d", "e:f"},
},
}
for _, tc := range testCases {
actual := modifySecurityOption(tc.config, tc.optName, tc.optVal)
if !reflect.DeepEqual(tc.expected, actual) {
t.Errorf("Failed to apply options correctly for tc: %s. Expected: %v but got %v", tc.name, tc.expected, actual)
}
}
}

View File

@ -0,0 +1,25 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["util.go"],
importpath = "k8s.io/kubernetes/pkg/kubelet/dockershim/testing",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,66 @@
/*
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"
)
// MemStore is an implementation of CheckpointStore interface which stores checkpoint in memory.
type MemStore struct {
mem map[string][]byte
sync.Mutex
}
func NewMemStore() *MemStore {
return &MemStore{mem: make(map[string][]byte)}
}
func (mstore *MemStore) Write(key string, data []byte) error {
mstore.Lock()
defer mstore.Unlock()
mstore.mem[key] = data
return nil
}
func (mstore *MemStore) Read(key string) ([]byte, error) {
mstore.Lock()
defer mstore.Unlock()
data, ok := mstore.mem[key]
if !ok {
return nil, fmt.Errorf("checkpoint is not found")
}
return data, nil
}
func (mstore *MemStore) Delete(key string) error {
mstore.Lock()
defer mstore.Unlock()
delete(mstore.mem, key)
return nil
}
func (mstore *MemStore) List() ([]string, error) {
mstore.Lock()
defer mstore.Unlock()
keys := make([]string, 0)
for key := range mstore.mem {
keys = append(keys, key)
}
return keys, nil
}