Merge pull request #16052 from ArtfulCoder/mount_etc_hosts

kubelet manages /etc/hosts file
pull/6/head
Filip Grzadkowski 2015-10-23 14:07:34 +02:00
commit 165169ab1c
5 changed files with 314 additions and 85 deletions

View File

@ -317,22 +317,6 @@ type containerStatusResult struct {
const podIPDownwardAPISelector = "status.podIP"
// podDependsOnIP returns whether any containers in a pod depend on using the pod IP via
// the downward API.
func podDependsOnPodIP(pod *api.Pod) bool {
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.ValueFrom != nil &&
env.ValueFrom.FieldRef != nil &&
env.ValueFrom.FieldRef.FieldPath == podIPDownwardAPISelector {
return true
}
}
}
return false
}
// determineContainerIP determines the IP address of the given container. It is expected
// that the container passed is the infrastructure container of a pod and the responsibility
// of the caller to ensure that the correct container is passed.
@ -1891,12 +1875,10 @@ func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, pod
glog.Warningf("Hairpin setup failed for pod %q: %v", podFullName, err)
}
}
if podDependsOnPodIP(pod) {
// Find the pod IP after starting the infra container in order to expose
// it safely via the downward API without a race.
pod.Status.PodIP = dm.determineContainerIP(pod.Name, pod.Namespace, podInfraContainer)
}
// Find the pod IP after starting the infra container in order to expose
// it safely via the downward API without a race and be able to use podIP in kubelet-managed /etc/hosts file.
pod.Status.PodIP = dm.determineContainerIP(pod.Name, pod.Namespace, podInfraContainer)
}
// Start everything

View File

@ -2059,61 +2059,3 @@ func TestGetIPCMode(t *testing.T) {
t.Errorf("expected host ipc mode for pod but got %v", ipcMode)
}
}
func TestPodDependsOnPodIP(t *testing.T) {
tests := []struct {
name string
expected bool
env api.EnvVar
}{
{
name: "depends on pod IP",
expected: true,
env: api.EnvVar{
Name: "POD_IP",
ValueFrom: &api.EnvVarSource{
FieldRef: &api.ObjectFieldSelector{
APIVersion: testapi.Default.Version(),
FieldPath: "status.podIP",
},
},
},
},
{
name: "literal value",
expected: false,
env: api.EnvVar{
Name: "SOME_VAR",
Value: "foo",
},
},
{
name: "other downward api field",
expected: false,
env: api.EnvVar{
Name: "POD_NAME",
ValueFrom: &api.EnvVarSource{
FieldRef: &api.ObjectFieldSelector{
APIVersion: testapi.Default.Version(),
FieldPath: "metadata.name",
},
},
},
},
}
for _, tc := range tests {
pod := &api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{
{Env: []api.EnvVar{tc.env}},
},
},
}
result := podDependsOnPodIP(pod)
if e, a := tc.expected, result; e != a {
t.Errorf("%v: Unexpected result; expected %v, got %v", tc.name, e, a)
}
}
}

View File

@ -20,6 +20,7 @@ package kubelet
// contrib/mesos/pkg/executor/.
import (
"bytes"
"errors"
"fmt"
"io"
@ -102,6 +103,8 @@ const (
// Minimum period for performing global cleanup tasks, i.e., housekeeping
// will not be performed more than once per housekeepingMinimumPeriod.
housekeepingMinimumPeriod = time.Second * 2
etcHostsPath = "/etc/hosts"
)
var (
@ -961,8 +964,17 @@ func (kl *Kubelet) syncNodeStatus() {
}
}
func makeMounts(container *api.Container, podVolumes kubecontainer.VolumeMap) (mounts []kubecontainer.Mount) {
func makeMounts(pod *api.Pod, podDir string, container *api.Container, podVolumes kubecontainer.VolumeMap) ([]kubecontainer.Mount, error) {
// Kubernetes only mounts on /etc/hosts if :
// - container does not use hostNetwork and
// - container is not a infrastructure(pause) container
// - container is not already mounting on /etc/hosts
// When the pause container is being created, its IP is still unknown. Hence, PodIP will not have been set.
mountEtcHostsFile := !pod.Spec.SecurityContext.HostNetwork && len(pod.Status.PodIP) > 0
glog.V(4).Infof("Will create hosts mount for container:%q, podIP:%s: %v", container.Name, pod.Status.PodIP, mountEtcHostsFile)
mounts := []kubecontainer.Mount{}
for _, mount := range container.VolumeMounts {
mountEtcHostsFile = mountEtcHostsFile && (mount.MountPath != etcHostsPath)
vol, ok := podVolumes[mount.Name]
if !ok {
glog.Warningf("Mount cannot be satisified for container %q, because the volume is missing: %q", container.Name, mount)
@ -975,7 +987,44 @@ func makeMounts(container *api.Container, podVolumes kubecontainer.VolumeMap) (m
ReadOnly: mount.ReadOnly,
})
}
return
if mountEtcHostsFile {
hostsMount, err := makeHostsMount(podDir, pod.Status.PodIP, pod.Name)
if err != nil {
return nil, err
}
mounts = append(mounts, *hostsMount)
}
return mounts, nil
}
func makeHostsMount(podDir, podIP, podName string) (*kubecontainer.Mount, error) {
hostsFilePath := path.Join(podDir, "etc-hosts")
if err := ensureHostsFile(hostsFilePath, podIP, podName); err != nil {
return nil, err
}
return &kubecontainer.Mount{
Name: "k8s-managed-etc-hosts",
ContainerPath: etcHostsPath,
HostPath: hostsFilePath,
ReadOnly: false,
}, nil
}
func ensureHostsFile(fileName string, hostIP, hostName string) error {
if _, err := os.Stat(fileName); os.IsExist(err) {
glog.V(4).Infof("kubernetes-managed etc-hosts file exits. Will not be recreated: %q", fileName)
return nil
}
var buffer bytes.Buffer
buffer.WriteString("# Kubernetes-managed hosts file.\n")
buffer.WriteString("127.0.0.1\tlocalhost\n") // ipv4 localhost
buffer.WriteString("::1\tlocalhost ip6-localhost ip6-loopback\n") // ipv6 localhost
buffer.WriteString("fe00::0\tip6-localnet\n")
buffer.WriteString("fe00::0\tip6-mcastprefix\n")
buffer.WriteString("fe00::1\tip6-allnodes\n")
buffer.WriteString("fe00::2\tip6-allrouters\n")
buffer.WriteString(fmt.Sprintf("%s\t%s\n", hostIP, hostName))
return ioutil.WriteFile(fileName, buffer.Bytes(), 0644)
}
func makePortMappings(container *api.Container) (ports []kubecontainer.PortMapping) {
@ -1020,7 +1069,10 @@ func (kl *Kubelet) GenerateRunContainerOptions(pod *api.Pod, container *api.Cont
}
opts.PortMappings = makePortMappings(container)
opts.Mounts = makeMounts(container, vol)
opts.Mounts, err = makeMounts(pod, kl.getPodDir(pod.UID), container, vol)
if err != nil {
return nil, err
}
opts.Envs, err = kl.makeEnvironmentVariables(pod, container)
if err != nil {
return nil, err

View File

@ -152,6 +152,11 @@ func newTestPods(count int) []*api.Pod {
pods := make([]*api.Pod, count)
for i := 0; i < count; i++ {
pods[i] = &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
HostNetwork: true,
},
},
ObjectMeta: api.ObjectMeta{
Name: fmt.Sprintf("pod%d", i),
},
@ -506,7 +511,7 @@ func TestMakeVolumeMounts(t *testing.T) {
container := api.Container{
VolumeMounts: []api.VolumeMount{
{
MountPath: "/mnt/path",
MountPath: "/etc/hosts",
Name: "disk",
ReadOnly: false,
},
@ -534,12 +539,20 @@ func TestMakeVolumeMounts(t *testing.T) {
"disk5": &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"},
}
mounts := makeMounts(&container, podVolumes)
pod := api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
HostNetwork: true,
},
},
}
mounts, _ := makeMounts(&pod, "/pod", &container, podVolumes)
expectedMounts := []kubecontainer.Mount{
{
"disk",
"/mnt/path",
"/etc/hosts",
"/mnt/disk",
false,
},

View File

@ -0,0 +1,240 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 e2e
import (
"fmt"
. "github.com/onsi/ginkgo"
api "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/latest"
"k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned"
"strings"
)
const (
kubeletEtcHostsImageName = "gcr.io/google_containers/netexec:1.0"
kubeletEtcHostsPodName = "test-pod"
kubeletEtcHostsHostNetworkPodName = "test-host-network-pod"
etcHostsPartialContent = "# Kubernetes-managed hosts file."
)
type KubeletManagedHostConfig struct {
hostNetworkPod *api.Pod
pod *api.Pod
f *Framework
}
var _ = Describe("KubeletManagedEtcHosts", func() {
f := NewFramework("e2e-kubelet-etc-hosts")
config := &KubeletManagedHostConfig{
f: f,
}
It("should test kubelet managed /etc/hosts file", func() {
By("Setting up the test")
config.setup()
By("Running the test")
config.verifyEtcHosts()
})
})
func (config *KubeletManagedHostConfig) verifyEtcHosts() {
By("Verifying /etc/hosts of container is kubelet-managed for pod with hostNetwork=false")
stdout := config.getEtcHostsContent(kubeletEtcHostsPodName, "busybox-1")
assertEtcHostsIsKubeletManaged(stdout)
stdout = config.getEtcHostsContent(kubeletEtcHostsPodName, "busybox-2")
assertEtcHostsIsKubeletManaged(stdout)
By("Verifying /etc/hosts of container is not kubelet-managed since container specifies /etc/hosts mount")
stdout = config.getEtcHostsContent(kubeletEtcHostsPodName, "busybox-3")
assertEtcHostsIsNotKubeletManaged(stdout)
By("Verifying /etc/hosts content of container is not kubelet-managed for pod with hostNetwork=true")
stdout = config.getEtcHostsContent(kubeletEtcHostsHostNetworkPodName, "busybox-1")
assertEtcHostsIsNotKubeletManaged(stdout)
stdout = config.getEtcHostsContent(kubeletEtcHostsHostNetworkPodName, "busybox-2")
assertEtcHostsIsNotKubeletManaged(stdout)
}
func (config *KubeletManagedHostConfig) setup() {
By("Creating hostNetwork=false pod")
config.createPodWithoutHostNetwork()
By("Creating hostNetwork=true pod")
config.createPodWithHostNetwork()
}
func (config *KubeletManagedHostConfig) createPodWithoutHostNetwork() {
podSpec := config.createPodSpec(kubeletEtcHostsPodName)
config.pod = config.createPod(podSpec)
}
func (config *KubeletManagedHostConfig) createPodWithHostNetwork() {
podSpec := config.createPodSpecWithHostNetwork(kubeletEtcHostsHostNetworkPodName)
config.hostNetworkPod = config.createPod(podSpec)
}
func (config *KubeletManagedHostConfig) createPod(podSpec *api.Pod) *api.Pod {
createdPod, err := config.getPodClient().Create(podSpec)
if err != nil {
Failf("Failed to create %s pod: %v", podSpec.Name, err)
}
expectNoError(config.f.WaitForPodRunning(podSpec.Name))
createdPod, err = config.getPodClient().Get(podSpec.Name)
if err != nil {
Failf("Failed to retrieve %s pod: %v", podSpec.Name, err)
}
return createdPod
}
func (config *KubeletManagedHostConfig) getPodClient() client.PodInterface {
return config.f.Client.Pods(config.f.Namespace.Name)
}
func assertEtcHostsIsKubeletManaged(etcHostsContent string) {
isKubeletManaged := strings.Contains(etcHostsContent, etcHostsPartialContent)
if !isKubeletManaged {
Failf("/etc/hosts file should be kubelet managed, but is not: %q", etcHostsContent)
}
}
func assertEtcHostsIsNotKubeletManaged(etcHostsContent string) {
isKubeletManaged := strings.Contains(etcHostsContent, etcHostsPartialContent)
if isKubeletManaged {
Failf("/etc/hosts file should not be kubelet managed, but is: %q", etcHostsContent)
}
}
func (config *KubeletManagedHostConfig) getEtcHostsContent(podName, containerName string) string {
cmd := kubectlCmd("exec", fmt.Sprintf("--namespace=%v", config.f.Namespace.Name), podName, "-c", containerName, "cat", "/etc/hosts")
stdout, stderr, err := startCmdAndStreamOutput(cmd)
if err != nil {
Failf("Failed to retrieve /etc/hosts, err: %q", err)
}
defer stdout.Close()
defer stderr.Close()
buf := make([]byte, 1000)
var n int
Logf("reading from `kubectl exec` command's stdout")
if n, err = stdout.Read(buf); err != nil {
Failf("Failed to read from kubectl exec stdout: %v", err)
}
return string(buf[:n])
}
func (config *KubeletManagedHostConfig) createPodSpec(podName string) *api.Pod {
pod := &api.Pod{
TypeMeta: unversioned.TypeMeta{
Kind: "Pod",
APIVersion: latest.GroupOrDie("").Version,
},
ObjectMeta: api.ObjectMeta{
Name: podName,
Namespace: config.f.Namespace.Name,
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "busybox-1",
Image: kubeletEtcHostsImageName,
ImagePullPolicy: api.PullIfNotPresent,
Command: []string{
"sleep",
"900",
},
},
{
Name: "busybox-2",
Image: kubeletEtcHostsImageName,
ImagePullPolicy: api.PullIfNotPresent,
Command: []string{
"sleep",
"900",
},
},
{
Name: "busybox-3",
Image: kubeletEtcHostsImageName,
ImagePullPolicy: api.PullIfNotPresent,
Command: []string{
"sleep",
"900",
},
VolumeMounts: []api.VolumeMount{
{
Name: "host-etc-hosts",
MountPath: "/etc/hosts",
},
},
},
},
Volumes: []api.Volume{
{
Name: "host-etc-hosts",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/etc/hosts",
},
},
},
},
},
}
return pod
}
func (config *KubeletManagedHostConfig) createPodSpecWithHostNetwork(podName string) *api.Pod {
pod := &api.Pod{
TypeMeta: unversioned.TypeMeta{
Kind: "Pod",
APIVersion: latest.GroupOrDie("").Version,
},
ObjectMeta: api.ObjectMeta{
Name: podName,
Namespace: config.f.Namespace.Name,
},
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
HostNetwork: true,
},
Containers: []api.Container{
{
Name: "busybox-1",
Image: kubeletEtcHostsImageName,
ImagePullPolicy: api.PullIfNotPresent,
Command: []string{
"sleep",
"900",
},
},
{
Name: "busybox-2",
Image: kubeletEtcHostsImageName,
ImagePullPolicy: api.PullIfNotPresent,
Command: []string{
"sleep",
"900",
},
},
},
},
}
return pod
}