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..9548ff39f --- /dev/null +++ b/validation/linux_ns_itype.go @@ -0,0 +1,131 @@ +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 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 { + 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 + // 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 { + nsPathAbs := filepath.Join(hostNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) + if err != nil { + 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 { + errNs = fmt.Errorf("cannot remove namespace %s: %v", nsName, err) + diagActual = fmt.Sprintf("err == %v", errNs) + diagExpected = "err == nil" + diagNsType = nsName + return errNs + } + } + + // 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 { + nsPathAbs := filepath.Join(containerNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) + if err != nil { + 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 { + // 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 errNs +} + +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/linux_ns_nopath.go b/validation/linux_ns_nopath.go new file mode 100644 index 000000000..a2f5b3ba3 --- /dev/null +++ b/validation/linux_ns_nopath.go @@ -0,0 +1,132 @@ +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 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 { + nsPathAbs := filepath.Join(hostNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) + if err != nil { + 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 { + 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(), + // 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 { + nsPathAbs := filepath.Join(containerNsPath, nsName) + nsInode, err := os.Readlink(nsPathAbs) + if err != nil { + 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 { + // 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 errNs +} + +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..5e4fc55c3 --- /dev/null +++ b/validation/util/linux_namespace.go @@ -0,0 +1,30 @@ +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", +} + +// 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 +}