Merge pull request #50 from Crazykev/resolv

Generate and maintain resolv.conf for sandbox
This commit is contained in:
Lantao Liu 2017-06-09 09:41:23 -07:00 committed by GitHub
commit 227dbe97f2
7 changed files with 160 additions and 20 deletions

View File

@ -18,6 +18,7 @@ package os
import ( import (
"io" "io"
"io/ioutil"
"os" "os"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -33,6 +34,7 @@ type OS interface {
OpenFifo(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) OpenFifo(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)
Stat(name string) (os.FileInfo, error) Stat(name string) (os.FileInfo, error)
CopyFile(src, dest string, perm os.FileMode) error CopyFile(src, dest string, perm os.FileMode) error
WriteFile(filename string, data []byte, perm os.FileMode) error
} }
// RealOS is used to dispatch the real system level operations. // RealOS is used to dispatch the real system level operations.
@ -75,3 +77,8 @@ func (RealOS) CopyFile(src, dest string, perm os.FileMode) error {
_, err = io.Copy(out, in) _, err = io.Copy(out, in)
return err return err
} }
// WriteFile will call ioutil.WriteFile to write data into a file.
func (RealOS) WriteFile(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}

View File

@ -36,6 +36,7 @@ type FakeOS struct {
OpenFifoFn func(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) OpenFifoFn func(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error)
StatFn func(string) (os.FileInfo, error) StatFn func(string) (os.FileInfo, error)
CopyFileFn func(string, string, os.FileMode) error CopyFileFn func(string, string, os.FileMode) error
WriteFileFn func(string, []byte, os.FileMode) error
errors map[string]error errors map[string]error
} }
@ -142,3 +143,15 @@ func (f *FakeOS) CopyFile(src, dest string, perm os.FileMode) error {
} }
return nil return nil
} }
// WriteFile is a fake call that invokes WriteFileFn or just return nil.
func (f *FakeOS) WriteFile(filename string, data []byte, perm os.FileMode) error {
if err := f.getError("WriteFile"); err != nil {
return err
}
if f.WriteFileFn != nil {
return f.WriteFileFn(filename, data, perm)
}
return nil
}

View File

@ -292,10 +292,6 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3
// TODO(random-liu): [P2] Add apparmor and seccomp. // TODO(random-liu): [P2] Add apparmor and seccomp.
// TODO(random-liu): [P1] Bind mount sandbox /dev/shm.
// TODO(random-liu): [P0] Bind mount sandbox resolv.conf.
return g.Spec(), nil return g.Spec(), nil
} }
@ -307,9 +303,17 @@ func (c *criContainerdService) generateContainerMounts(sandboxRootDir string, co
mounts = append(mounts, &runtime.Mount{ mounts = append(mounts, &runtime.Mount{
ContainerPath: etcHosts, ContainerPath: etcHosts,
HostPath: getSandboxHosts(sandboxRootDir), HostPath: getSandboxHosts(sandboxRootDir),
Readonly: securityContext.ReadonlyRootfs, Readonly: securityContext.GetReadonlyRootfs(),
}) })
// TODO(random-liu): [P0] Mount sandbox resolv.config.
// Mount sandbox resolv.config.
// TODO: Need to figure out whether we should always mount it as read-only
mounts = append(mounts, &runtime.Mount{
ContainerPath: resolvConfPath,
HostPath: getResolvPath(sandboxRootDir),
Readonly: securityContext.GetReadonlyRootfs(),
})
// TODO(random-liu): [P0] Mount sandbox /dev/shm. // TODO(random-liu): [P0] Mount sandbox /dev/shm.
return mounts return mounts
} }

View File

@ -309,23 +309,37 @@ func TestGenerateContainerMounts(t *testing.T) {
securityContext *runtime.LinuxContainerSecurityContext securityContext *runtime.LinuxContainerSecurityContext
expectedMounts []*runtime.Mount expectedMounts []*runtime.Mount
}{ }{
"should setup ro /etc/hosts mount when rootfs is read-only": { "should setup ro mount when rootfs is read-only": {
securityContext: &runtime.LinuxContainerSecurityContext{ securityContext: &runtime.LinuxContainerSecurityContext{
ReadonlyRootfs: true, ReadonlyRootfs: true,
}, },
expectedMounts: []*runtime.Mount{{ expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts", ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts", HostPath: testSandboxRootDir + "/hosts",
Readonly: true, Readonly: true,
}},
}, },
"should setup rw /etc/hosts mount when rootfs is read-write": { {
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: true,
},
},
},
"should setup rw mount when rootfs is read-write": {
securityContext: &runtime.LinuxContainerSecurityContext{}, securityContext: &runtime.LinuxContainerSecurityContext{},
expectedMounts: []*runtime.Mount{{ expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts", ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts", HostPath: testSandboxRootDir + "/hosts",
Readonly: false, Readonly: false,
}}, },
{
ContainerPath: resolvConfPath,
HostPath: getResolvPath(testSandboxRootDir),
Readonly: false,
},
},
}, },
} { } {
config := &runtime.ContainerConfig{ config := &runtime.ContainerConfig{

View File

@ -69,6 +69,9 @@ const (
sandboxesDir = "sandboxes" sandboxesDir = "sandboxes"
// containersDir contains all container root. // containersDir contains all container root.
containersDir = "containers" containersDir = "containers"
// According to http://man7.org/linux/man-pages/man5/resolv.conf.5.html:
// "The search list is currently limited to six domains with a total of 256 characters."
maxDNSSearches = 6
// stdinNamedPipe is the name of stdin named pipe. // stdinNamedPipe is the name of stdin named pipe.
stdinNamedPipe = "stdin" stdinNamedPipe = "stdin"
// stdoutNamedPipe is the name of stdout named pipe. // stdoutNamedPipe is the name of stdout named pipe.
@ -87,6 +90,8 @@ const (
pidNSFormat = "/proc/%v/ns/pid" pidNSFormat = "/proc/%v/ns/pid"
// etcHosts is the default path of /etc/hosts file. // etcHosts is the default path of /etc/hosts file.
etcHosts = "/etc/hosts" etcHosts = "/etc/hosts"
// resolvConfPath is the abs path of resolv.conf on host or container.
resolvConfPath = "/etc/resolv.conf"
) )
// generateID generates a random unique id. // generateID generates a random unique id.
@ -149,6 +154,11 @@ func getSandboxHosts(sandboxRootDir string) string {
return filepath.Join(sandboxRootDir, "hosts") return filepath.Join(sandboxRootDir, "hosts")
} }
// getResolvPath returns resolv.conf filepath for specified sandbox.
func getResolvPath(sandboxRoot string) string {
return filepath.Join(sandboxRoot, "resolv.conf")
}
// prepareStreamingPipes prepares stream named pipe for container. returns nil // prepareStreamingPipes prepares stream named pipe for container. returns nil
// streaming handler if corresponding stream path is empty. // streaming handler if corresponding stream path is empty.
func (c *criContainerdService) prepareStreamingPipes(ctx context.Context, stdin, stdout, stderr string) ( func (c *criContainerdService) prepareStreamingPipes(ctx context.Context, stdin, stdout, stderr string) (

View File

@ -19,6 +19,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/containerd/containerd/api/services/execution" "github.com/containerd/containerd/api/services/execution"
@ -303,8 +304,55 @@ func (c *criContainerdService) setupSandboxFiles(rootDir string, config *runtime
if err := c.os.CopyFile(etcHosts, sandboxEtcHosts, 0666); err != nil { if err := c.os.CopyFile(etcHosts, sandboxEtcHosts, 0666); err != nil {
return fmt.Errorf("failed to generate sandbox hosts file %q: %v", sandboxEtcHosts, err) return fmt.Errorf("failed to generate sandbox hosts file %q: %v", sandboxEtcHosts, err)
} }
// TODO(random-liu): [P0] Set DNS options. Maintain a resolv.conf for the sandbox.
// Set DNS options. Maintain a resolv.conf for the sandbox.
var err error
resolvContent := ""
if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
resolvContent, err = parseDNSOptions(dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options)
if err != nil {
return fmt.Errorf("failed to parse sandbox DNSConfig %+v: %v", dnsConfig, err)
}
}
resolvPath := getResolvPath(rootDir)
if resolvContent == "" {
// copy host's resolv.conf to resolvPath
err = c.os.CopyFile(resolvConfPath, resolvPath, 0644)
if err != nil {
return fmt.Errorf("failed to copy host's resolv.conf to %q: %v", resolvPath, err)
}
} else {
err = c.os.WriteFile(resolvPath, []byte(resolvContent), 0644)
if err != nil {
return fmt.Errorf("failed to write resolv content to %q: %v", resolvPath, err)
}
}
// TODO(random-liu): [P0] Deal with /dev/shm. Use host for HostIpc, and create and mount for // TODO(random-liu): [P0] Deal with /dev/shm. Use host for HostIpc, and create and mount for
// non-HostIpc. What about mqueue? // non-HostIpc. What about mqueue?
return nil return nil
} }
// parseDNSOptions parse DNS options into resolv.conf format content,
// if none option is specified, will return empty with no error.
func parseDNSOptions(servers, searches, options []string) (string, error) {
resolvContent := ""
if len(searches) > maxDNSSearches {
return "", fmt.Errorf("DNSOption.Searches has more than 6 domains")
}
if len(searches) > 0 {
resolvContent += fmt.Sprintf("search %s\n", strings.Join(searches, " "))
}
if len(servers) > 0 {
resolvContent += fmt.Sprintf("nameserver %s\n", strings.Join(servers, "\nnameserver "))
}
if len(options) > 0 {
resolvContent += fmt.Sprintf("options %s\n", strings.Join(options, " "))
}
return resolvContent, nil
}

View File

@ -161,6 +161,7 @@ func TestSetupSandboxFiles(t *testing.T) {
testRootDir := "test-sandbox-root" testRootDir := "test-sandbox-root"
expectedCopys := [][]interface{}{ expectedCopys := [][]interface{}{
{"/etc/hosts", testRootDir + "/hosts", os.FileMode(0666)}, {"/etc/hosts", testRootDir + "/hosts", os.FileMode(0666)},
{"/etc/resolv.conf", testRootDir + "/resolv.conf", os.FileMode(0644)},
} }
c := newTestCRIContainerdService() c := newTestCRIContainerdService()
var copys [][]interface{} var copys [][]interface{}
@ -169,7 +170,7 @@ func TestSetupSandboxFiles(t *testing.T) {
return nil return nil
} }
c.setupSandboxFiles(testRootDir, nil) c.setupSandboxFiles(testRootDir, nil)
assert.Equal(t, expectedCopys, copys, "should copy /etc/hosts for sandbox") assert.Equal(t, expectedCopys, copys, "should copy expected files for sandbox")
} }
func TestRunPodSandbox(t *testing.T) { func TestRunPodSandbox(t *testing.T) {
@ -269,5 +270,48 @@ func TestRunPodSandbox(t *testing.T) {
assert.Equal(t, expectedPluginArgument, pluginArgument, "SetUpPod should be called with correct arguments") assert.Equal(t, expectedPluginArgument, pluginArgument, "SetUpPod should be called with correct arguments")
} }
func TestParseDNSOption(t *testing.T) {
for desc, test := range map[string]struct {
servers []string
searches []string
options []string
expectedContent string
expectErr bool
}{
"empty dns options should return empty content": {},
"non-empty dns options should return correct content": {
servers: []string{"8.8.8.8", "server.google.com"},
searches: []string{"114.114.114.114"},
options: []string{"timeout:1"},
expectedContent: `search 114.114.114.114
nameserver 8.8.8.8
nameserver server.google.com
options timeout:1
`,
},
"should return error if dns search exceeds limit(6)": {
searches: []string{
"server0.google.com",
"server1.google.com",
"server2.google.com",
"server3.google.com",
"server4.google.com",
"server5.google.com",
"server6.google.com",
},
expectErr: true,
},
} {
t.Logf("TestCase %q", desc)
resolvContent, err := parseDNSOptions(test.servers, test.searches, test.options)
if test.expectErr {
assert.Error(t, err)
continue
}
assert.NoError(t, err)
assert.Equal(t, resolvContent, test.expectedContent)
}
}
// TODO(random-liu): [P1] Add unit test for different error cases to make sure // TODO(random-liu): [P1] Add unit test for different error cases to make sure
// the function cleans up on error properly. // the function cleans up on error properly.