From 5ce0ff85a6ef4560c656d55088a361608a9c8e86 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Thu, 12 Apr 2018 17:11:01 +0200 Subject: [PATCH 1/4] validation: add a new test for NSNewNSWithoutPath This test is to check for NSNewNSWithoutPath, i.e. "If path is not specified, the runtime MUST create a new container namespace of the given type" See also https://github.com/opencontainers/runtime-tools/issues/572 Signed-off-by: Dongsu Park --- validation/linux_ns_nopath.go | 92 ++++++++++++++++++++++++++++++ validation/util/linux_namespace.go | 14 +++++ 2 files changed, 106 insertions(+) create mode 100644 validation/linux_ns_nopath.go create mode 100644 validation/util/linux_namespace.go diff --git a/validation/linux_ns_nopath.go b/validation/linux_ns_nopath.go new file mode 100644 index 000000000..6f15fcb0a --- /dev/null +++ b/validation/linux_ns_nopath.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/mndrix/tap-go" + rspec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/specerror" + "github.com/opencontainers/runtime-tools/validation/util" +) + +func testNamespaceNoPath(t *tap.T) error { + hostNsPath := fmt.Sprintf("/proc/%d/ns", os.Getpid()) + hostNsInodes := map[string]string{} + for _, nsName := range util.ProcNamespaces { + nsInode, err := os.Readlink(filepath.Join(hostNsPath, nsName)) + if err != nil { + return err + } + hostNsInodes[nsName] = nsInode + } + + g, err := util.GetDefaultGenerator() + if err != nil { + return err + } + + // As the namespaces, cgroups and user, are not set by GetDefaultGenerator(), + // others are set by default. We just set them explicitly to avoid confusion. + g.AddOrReplaceLinuxNamespace("cgroup", "") + g.AddOrReplaceLinuxNamespace("ipc", "") + g.AddOrReplaceLinuxNamespace("mount", "") + g.AddOrReplaceLinuxNamespace("network", "") + g.AddOrReplaceLinuxNamespace("pid", "") + g.AddOrReplaceLinuxNamespace("user", "") + g.AddOrReplaceLinuxNamespace("uts", "") + + // For user namespaces, we need to set uid/gid maps to create a container + g.AddLinuxUIDMapping(uint32(1000), uint32(0), uint32(1000)) + g.AddLinuxGIDMapping(uint32(1000), uint32(0), uint32(1000)) + + err = util.RuntimeOutsideValidate(g, func(config *rspec.Spec, state *rspec.State) error { + containerNsPath := fmt.Sprintf("/proc/%d/ns", state.Pid) + + for _, nsName := range util.ProcNamespaces { + nsInode, err := os.Readlink(filepath.Join(containerNsPath, nsName)) + if err != nil { + return err + } + + t.Ok(hostNsInodes[nsName] != nsInode, fmt.Sprintf("create namespace %s without path", nsName)) + if hostNsInodes[nsName] == nsInode { + specErr := specerror.NewError(specerror.NSNewNSWithoutPath, + fmt.Errorf("both namespaces for %s have the same inode %s", nsName, nsInode), + rspec.Version) + diagnostic := map[string]interface{}{ + "expected": fmt.Sprintf("!= %s", hostNsInodes[nsName]), + "actual": nsInode, + "namespace type": nsName, + "level": specErr.(*specerror.Error).Err.Level, + "reference": specErr.(*specerror.Error).Err.Reference, + } + t.YAML(diagnostic) + + continue + } + } + + return nil + }) + + return err +} + +func main() { + t := tap.New() + t.Header(0) + + if "linux" != runtime.GOOS { + t.Skip(1, fmt.Sprintf("linux-specific namespace test")) + } + + err := testNamespaceNoPath(t) + if err != nil { + util.Fatal(err) + } + + t.AutoPlan() +} diff --git a/validation/util/linux_namespace.go b/validation/util/linux_namespace.go new file mode 100644 index 000000000..3d434b425 --- /dev/null +++ b/validation/util/linux_namespace.go @@ -0,0 +1,14 @@ +package util + +// ProcNamespaces defines a list of namespaces to be found under /proc/*/ns/. +// NOTE: it is not the same as generate.Namespaces, because of naming +// mismatches like "mnt" vs "mount" or "net" vs "network". +var ProcNamespaces = []string{ + "cgroup", + "ipc", + "mnt", + "net", + "pid", + "user", + "uts", +} From d7985e31188a6a75d94daabda02a9308bb22da04 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Fri, 13 Apr 2018 13:50:08 +0200 Subject: [PATCH 2/4] validation: add a new test for NSInheritWithoutType This test is to check for NSInheritWithoutType, i.e. "If a namespace type is not specified in the namespaces array, the container MUST inherit the runtime namespace of that type". See also https://github.com/opencontainers/runtime-tools/issues/572 Signed-off-by: Dongsu Park --- generate/generate.go | 8 +++ validation/linux_ns_itype.go | 88 ++++++++++++++++++++++++++++++ validation/util/linux_namespace.go | 16 ++++++ 3 files changed, 112 insertions(+) create mode 100644 validation/linux_ns_itype.go diff --git a/generate/generate.go b/generate/generate.go index 21da8a226..07b57b44b 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -370,6 +370,14 @@ func (g *Generator) RemoveAnnotation(key string) { delete(g.Config.Annotations, key) } +// RemoveHostname removes g.Config.Hostname, setting it to an empty string. +func (g *Generator) RemoveHostname() { + if g.Config == nil { + return + } + g.Config.Hostname = "" +} + // SetProcessConsoleSize sets g.Config.Process.ConsoleSize. func (g *Generator) SetProcessConsoleSize(width, height uint) { g.initConfigProcessConsoleSize() diff --git a/validation/linux_ns_itype.go b/validation/linux_ns_itype.go new file mode 100644 index 000000000..bb9c85217 --- /dev/null +++ b/validation/linux_ns_itype.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/mndrix/tap-go" + rspec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/specerror" + "github.com/opencontainers/runtime-tools/validation/util" +) + +func testNamespaceInheritType(t *tap.T) error { + g, err := util.GetDefaultGenerator() + if err != nil { + return err + } + + // Obtain a map for host (runtime) namespace, and remove every namespace + // from the generated config, to be able to see if each container namespace + // becomes inherited from its corresponding host namespace. + hostNsPath := fmt.Sprintf("/proc/%d/ns", os.Getpid()) + hostNsInodes := map[string]string{} + for _, nsName := range util.ProcNamespaces { + nsInode, err := os.Readlink(filepath.Join(hostNsPath, nsName)) + if err != nil { + return err + } + hostNsInodes[nsName] = nsInode + + if err := g.RemoveLinuxNamespace(util.GetRuntimeToolsNamespace(nsName)); err != nil { + return err + } + } + + // We need to remove hostname to avoid test failures when not creating UTS namespace + g.RemoveHostname() + + err = util.RuntimeOutsideValidate(g, func(config *rspec.Spec, state *rspec.State) error { + containerNsPath := fmt.Sprintf("/proc/%d/ns", state.Pid) + + for _, nsName := range util.ProcNamespaces { + nsInode, err := os.Readlink(filepath.Join(containerNsPath, nsName)) + if err != nil { + return err + } + + t.Ok(hostNsInodes[nsName] == nsInode, fmt.Sprintf("inherit namespace %s without type", nsName)) + if hostNsInodes[nsName] != nsInode { + specErr := specerror.NewError(specerror.NSInheritWithoutType, + fmt.Errorf("namespace %s (inode %s) does not inherit runtime namespace %s", nsName, nsInode, hostNsInodes[nsName]), + rspec.Version) + diagnostic := map[string]interface{}{ + "expected": hostNsInodes[nsName], + "actual": nsInode, + "namespace type": nsName, + "level": specErr.(*specerror.Error).Err.Level, + "reference": specErr.(*specerror.Error).Err.Reference, + } + t.YAML(diagnostic) + + continue + } + } + + return nil + }) + + return err +} + +func main() { + t := tap.New() + t.Header(0) + + if "linux" != runtime.GOOS { + t.Skip(1, fmt.Sprintf("linux-specific namespace test")) + } + + err := testNamespaceInheritType(t) + if err != nil { + util.Fatal(err) + } + + t.AutoPlan() +} diff --git a/validation/util/linux_namespace.go b/validation/util/linux_namespace.go index 3d434b425..5e4fc55c3 100644 --- a/validation/util/linux_namespace.go +++ b/validation/util/linux_namespace.go @@ -12,3 +12,19 @@ var ProcNamespaces = []string{ "user", "uts", } + +// GetRuntimeToolsNamespace converts a namespace type string for /proc into +// a string for runtime-tools. It deals with exceptional cases of "net" and +// "mnt", because those strings cannot be recognized by mapStrToNamespace(), +// which actually expects "network" and "mount" respectively. +func GetRuntimeToolsNamespace(ns string) string { + switch ns { + case "net": + return "network" + case "mnt": + return "mount" + } + + // In other cases, return just the original string + return ns +} From 1ac1c0265d8c8795e06af8ecf30a039d41e34d58 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Mon, 23 Apr 2018 12:15:34 +0200 Subject: [PATCH 3/4] validation: split out pringDiag from testNamespaceNoPath To be able to deal with diagnostics in a general way, split out the diagnostics handling code into a new function `pringDiag()`, which will be normally called from the defer function's context. This way we can simplify the error handling routines. Signed-off-by: Dongsu Park --- validation/linux_ns_nopath.go | 76 ++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/validation/linux_ns_nopath.go b/validation/linux_ns_nopath.go index 6f15fcb0a..a2f5b3ba3 100644 --- a/validation/linux_ns_nopath.go +++ b/validation/linux_ns_nopath.go @@ -12,20 +12,58 @@ import ( "github.com/opencontainers/runtime-tools/validation/util" ) +func printDiag(t *tap.T, diagActual, diagExpected, diagNsType string, errNs error) { + specErr := specerror.NewError(specerror.NSNewNSWithoutPath, + errNs, rspec.Version) + diagnostic := map[string]string{ + "actual": diagActual, + "expected": diagExpected, + "namespace type": diagNsType, + "level": specErr.(*specerror.Error).Err.Level.String(), + "reference": specErr.(*specerror.Error).Err.Reference, + } + t.YAML(diagnostic) +} + func testNamespaceNoPath(t *tap.T) error { + var errNs error + diagActual := "" + diagExpected := "" + diagNsType := "" + + // To be able to print out diagnostics for all kinds of error cases + // at the end of the tests, we make use of defer function. To do that, + // each error handling routine should set diagActual, diagExpected, + // diagNsType, and errNs, before returning an error. + defer func() { + if errNs != nil { + printDiag(t, diagActual, diagExpected, diagNsType, errNs) + } + }() + hostNsPath := fmt.Sprintf("/proc/%d/ns", os.Getpid()) hostNsInodes := map[string]string{} + for _, nsName := range util.ProcNamespaces { - nsInode, err := os.Readlink(filepath.Join(hostNsPath, nsName)) + nsPathAbs := filepath.Join(hostNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) if err != nil { - return err + errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs } hostNsInodes[nsName] = nsInode } g, err := util.GetDefaultGenerator() if err != nil { - return err + errNs = fmt.Errorf("cannot get the default generator: %v", err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + // NOTE: we don't have a namespace type + return errNs } // As the namespaces, cgroups and user, are not set by GetDefaultGenerator(), @@ -46,33 +84,35 @@ func testNamespaceNoPath(t *tap.T) error { containerNsPath := fmt.Sprintf("/proc/%d/ns", state.Pid) for _, nsName := range util.ProcNamespaces { - nsInode, err := os.Readlink(filepath.Join(containerNsPath, nsName)) + nsPathAbs := filepath.Join(containerNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) if err != nil { - return err + errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs } t.Ok(hostNsInodes[nsName] != nsInode, fmt.Sprintf("create namespace %s without path", nsName)) if hostNsInodes[nsName] == nsInode { - specErr := specerror.NewError(specerror.NSNewNSWithoutPath, - fmt.Errorf("both namespaces for %s have the same inode %s", nsName, nsInode), - rspec.Version) - diagnostic := map[string]interface{}{ - "expected": fmt.Sprintf("!= %s", hostNsInodes[nsName]), - "actual": nsInode, - "namespace type": nsName, - "level": specErr.(*specerror.Error).Err.Level, - "reference": specErr.(*specerror.Error).Err.Reference, - } - t.YAML(diagnostic) - + // NOTE: for such inode match cases, we should print out diagnostics + // for each case, not only at the end of tests. So we should simply + // call once printDiag(), then continue testing next namespaces. + // Thus we don't need to set diagActual, diagExpected, diagNsType, etc. + printDiag(t, nsInode, fmt.Sprintf("!= %s", hostNsInodes[nsName]), nsName, + fmt.Errorf("both namespaces for %s have the same inode %s", nsName, nsInode)) continue } } return nil }) + if err != nil { + errNs = fmt.Errorf("cannot run validation tests: %v", err) + } - return err + return errNs } func main() { From e830fa33489f34ffd79c1bacc273c44a8d00199d Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Mon, 23 Apr 2018 15:22:57 +0200 Subject: [PATCH 4/4] validation: split out pringDiag from testNamespaceInheritType To be able to deal with diagnostics in a general way, split out the diagnostics handling code into a new function `pringDiag()`, which will be normally called from the defer function's context. This way we can simplify the error handling routines. Signed-off-by: Dongsu Park --- validation/linux_ns_itype.go | 81 +++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/validation/linux_ns_itype.go b/validation/linux_ns_itype.go index bb9c85217..9548ff39f 100644 --- a/validation/linux_ns_itype.go +++ b/validation/linux_ns_itype.go @@ -12,10 +12,42 @@ import ( "github.com/opencontainers/runtime-tools/validation/util" ) +func printDiag(t *tap.T, diagActual, diagExpected, diagNsType string, errNs error) { + specErr := specerror.NewError(specerror.NSInheritWithoutType, + errNs, rspec.Version) + diagnostic := map[string]string{ + "actual": diagActual, + "expected": diagExpected, + "namespace type": diagNsType, + "level": specErr.(*specerror.Error).Err.Level.String(), + "reference": specErr.(*specerror.Error).Err.Reference, + } + t.YAML(diagnostic) +} + func testNamespaceInheritType(t *tap.T) error { + var errNs error + diagActual := "" + diagExpected := "" + diagNsType := "" + + // To be able to print out diagnostics for all kinds of error cases + // at the end of the tests, we make use of defer function. To do that, + // each error handling routine should set diagActual, diagExpected, + // diagNsType, and errNs, before returning an error. + defer func() { + if errNs != nil { + printDiag(t, diagActual, diagExpected, diagNsType, errNs) + } + }() + g, err := util.GetDefaultGenerator() if err != nil { - return err + errNs = fmt.Errorf("cannot get the default generator: %v", err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + // NOTE: we don't have a namespace type + return errNs } // Obtain a map for host (runtime) namespace, and remove every namespace @@ -24,14 +56,23 @@ func testNamespaceInheritType(t *tap.T) error { hostNsPath := fmt.Sprintf("/proc/%d/ns", os.Getpid()) hostNsInodes := map[string]string{} for _, nsName := range util.ProcNamespaces { - nsInode, err := os.Readlink(filepath.Join(hostNsPath, nsName)) + nsPathAbs := filepath.Join(hostNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) if err != nil { - return err + errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs } hostNsInodes[nsName] = nsInode if err := g.RemoveLinuxNamespace(util.GetRuntimeToolsNamespace(nsName)); err != nil { - return err + errNs = fmt.Errorf("cannot remove namespace %s: %v", nsName, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs } } @@ -42,33 +83,35 @@ func testNamespaceInheritType(t *tap.T) error { containerNsPath := fmt.Sprintf("/proc/%d/ns", state.Pid) for _, nsName := range util.ProcNamespaces { - nsInode, err := os.Readlink(filepath.Join(containerNsPath, nsName)) + nsPathAbs := filepath.Join(containerNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) if err != nil { - return err + errNs = fmt.Errorf("cannot resolve symlink %q: %v", nsPathAbs, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs } t.Ok(hostNsInodes[nsName] == nsInode, fmt.Sprintf("inherit namespace %s without type", nsName)) if hostNsInodes[nsName] != nsInode { - specErr := specerror.NewError(specerror.NSInheritWithoutType, - fmt.Errorf("namespace %s (inode %s) does not inherit runtime namespace %s", nsName, nsInode, hostNsInodes[nsName]), - rspec.Version) - diagnostic := map[string]interface{}{ - "expected": hostNsInodes[nsName], - "actual": nsInode, - "namespace type": nsName, - "level": specErr.(*specerror.Error).Err.Level, - "reference": specErr.(*specerror.Error).Err.Reference, - } - t.YAML(diagnostic) - + // NOTE: for such inode match cases, we should print out diagnostics + // for each case, not only at the end of tests. So we should simply + // call once printDiag(), then continue testing next namespaces. + // Thus we don't need to set diagActual, diagExpected, diagNsType, etc. + printDiag(t, nsInode, hostNsInodes[nsName], nsName, + fmt.Errorf("namespace %s (inode %s) does not inherit runtime namespace %s", nsName, nsInode, hostNsInodes[nsName])) continue } } return nil }) + if err != nil { + errNs = fmt.Errorf("cannot run validation tests: %v", err) + } - return err + return errNs } func main() {