github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/nsfix/nsfix_test.go (about)

     1  /*
     2  Copyright 2018 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package nsfix
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"runtime"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang/glog"
    32  
    33  	"github.com/Mirantis/virtlet/pkg/utils"
    34  	testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
    35  )
    36  
    37  type nsFixTestArg struct {
    38  	DirPath string
    39  }
    40  
    41  type nsFixTestRet struct {
    42  	Files  []string
    43  	IsRoot bool
    44  }
    45  
    46  func listFiles(dirPath string) ([]string, error) {
    47  	matches, err := filepath.Glob(filepath.Join(dirPath, "*"))
    48  	if err != nil {
    49  		return nil, fmt.Errorf("Glob(): %v", err)
    50  	}
    51  
    52  	var r []string
    53  	for _, m := range matches {
    54  		r = append(r, filepath.Base(m))
    55  	}
    56  	return r, nil
    57  }
    58  
    59  func handleNsFixTest1(data interface{}) (interface{}, error) {
    60  	arg := data.(*nsFixTestArg)
    61  	files, err := listFiles(arg.DirPath)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	return nsFixTestRet{files, os.Getuid() == 0}, nil
    66  }
    67  
    68  func handleNsFixTest2(data interface{}) (interface{}, error) {
    69  	arg := data.(*nsFixTestArg)
    70  	files, err := listFiles(arg.DirPath)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	contents := []byte(strings.Join(files, "\n"))
    76  	if err := ioutil.WriteFile(filepath.Join(arg.DirPath, "out"), contents, 0666); err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	return nil, nil
    81  }
    82  
    83  func handleNsFixWithNilArg(data interface{}) (interface{}, error) {
    84  	return 42, nil
    85  }
    86  
    87  func handleNsFixWithNilResult(data interface{}) (interface{}, error) {
    88  	targetPath := data.(*string)
    89  	return nil, os.Mkdir(filepath.Join(*targetPath, "foobar"), 0777)
    90  }
    91  
    92  func verifyNsFix(t *testing.T, toRun func(tmpDir string, dirs map[string]string, pids []int)) {
    93  	if runtime.GOOS != "linux" {
    94  		t.Skip("The namespace fix only works on Linux")
    95  	}
    96  	if os.Getuid() != 0 {
    97  		t.Skip("This test requires root privs")
    98  	}
    99  
   100  	tmpDir, err := ioutil.TempDir("", "nsfix-")
   101  	if err != nil {
   102  		t.Fatalf("Can't create temp dir for config image: %v", err)
   103  	}
   104  	defer os.RemoveAll(tmpDir)
   105  
   106  	if err := os.Chmod(tmpDir, 0755); err != nil {
   107  		t.Fatalf("Chown(): %v", err)
   108  	}
   109  
   110  	// TEST_NSFIX is handled by init() below
   111  	os.Setenv("TEST_NSFIX", "1")
   112  	defer os.Setenv("TEST_NSFIX", "")
   113  
   114  	dirA := filepath.Join(tmpDir, "a")
   115  	dirD := filepath.Join(tmpDir, "d")
   116  	dirs := map[string]string{
   117  		"a": dirA,
   118  		"b": filepath.Join(tmpDir, "b"),
   119  		"c": filepath.Join(dirA, "c"),
   120  		"d": dirD,
   121  		"e": filepath.Join(dirD, "e"),
   122  	}
   123  	for _, p := range []string{"a", "b", "c", "d", "e"} {
   124  		if err := os.Mkdir(dirs[p], 0777); err != nil {
   125  			t.Fatalf("Can't create dir %q: %v", p, err)
   126  		}
   127  	}
   128  
   129  	doneFiles := []string{
   130  		filepath.Join(tmpDir, "done1"),
   131  		filepath.Join(tmpDir, "done2"),
   132  	}
   133  	var pids []int
   134  	for _, cmd := range []string{
   135  		fmt.Sprintf("mount --bind %q %q && touch %q && sleep 10000", dirs["a"], dirs["b"], doneFiles[0]),
   136  		fmt.Sprintf("mount --bind %q %q && touch %q && sleep 10000", dirs["d"], dirs["b"], doneFiles[1]),
   137  	} {
   138  		tc := testutils.RunProcess(t, "unshare", []string{
   139  			"-m", "/bin/bash", "-c", cmd,
   140  		}, nil)
   141  		defer tc.Stop()
   142  		pids = append(pids, tc.Pid())
   143  	}
   144  	if err := utils.WaitLoop(func() (bool, error) {
   145  		for _, fileName := range doneFiles {
   146  			switch _, err := os.Stat(fileName); {
   147  			case err == nil:
   148  				// ok
   149  			case os.IsNotExist(err):
   150  				return false, nil
   151  			default:
   152  				return false, err
   153  			}
   154  		}
   155  		return true, nil
   156  	}, 50*time.Millisecond, 10*time.Second, nil); err != nil {
   157  		t.Fatal(err)
   158  	}
   159  
   160  	toRun(tmpDir, dirs, pids)
   161  }
   162  
   163  func TestNsFix(t *testing.T) {
   164  	verifyNsFix(t, func(tmpDir string, dirs map[string]string, pids []int) {
   165  		var r nsFixTestRet
   166  		if err := NewCall("nsFixTest1").
   167  			TargetPid(pids[0]).
   168  			Arg(nsFixTestArg{dirs["b"]}).
   169  			SpawnInNamespaces(&r); err != nil {
   170  			t.Fatalf("SpawnInNamespaces(): %v", err)
   171  		}
   172  		resultStr := strings.Join(r.Files, "\n")
   173  		expectedResultStr := "c"
   174  		if resultStr != expectedResultStr {
   175  			t.Errorf("Bad result from SpawnInNamespaces(): %q instead of %q", resultStr, expectedResultStr)
   176  		}
   177  		if !r.IsRoot {
   178  			t.Errorf("SpawnInNamespaces dropped privs when not requested to do so")
   179  		}
   180  
   181  		if err := NewCall("nsFixTest1").
   182  			TargetPid(pids[1]).
   183  			Arg(nsFixTestArg{dirs["b"]}).
   184  			DropPrivs().
   185  			SpawnInNamespaces(&r); err != nil {
   186  			t.Fatalf("SpawnInNamespaces(): %v", err)
   187  		}
   188  		resultStr = strings.Join(r.Files, "\n")
   189  		expectedResultStr = "e"
   190  		if resultStr != expectedResultStr {
   191  			t.Errorf("Bad result from SpawnInNamespaces(): %q instead of %q", resultStr, expectedResultStr)
   192  		}
   193  		if r.IsRoot {
   194  			t.Errorf("SpawnInNamespaces didn't drop privs not requested to do so")
   195  		}
   196  
   197  		// we examine SwitchToNamespace in the same test to make
   198  		// sure the calls don't interfere between themselves
   199  		cmd := exec.Command(os.Args[0])
   200  		cmd.Stderr = os.Stderr
   201  		cmd.Stdout = os.Stdout
   202  		cmd.Env = append(os.Environ(), fmt.Sprintf("TEST_NSFIX_SWITCH=%d:%s", pids[1], dirs["b"]))
   203  		if err := cmd.Run(); err != nil {
   204  			if exitErr, ok := err.(*exec.ExitError); ok {
   205  				t.Errorf("Error rerunning the test executable: %v\nstderr:\n%s", err, exitErr.Stderr)
   206  			} else {
   207  				t.Errorf("Error rerunning the test executable: %v", err)
   208  			}
   209  		}
   210  
   211  		outPath := filepath.Join(dirs["d"], "out")
   212  		expectedContents := "e"
   213  		switch outStr, err := ioutil.ReadFile(outPath); {
   214  		case err != nil:
   215  			t.Errorf("Error reading %q: %v", outPath, err)
   216  		case string(outStr) != expectedContents:
   217  			t.Errorf("bad out file contents: %q instead of %q", outStr, expectedContents)
   218  		}
   219  	})
   220  }
   221  
   222  func TestNsFixWithNilArg(t *testing.T) {
   223  	verifyNsFix(t, func(tmpDir string, dirs map[string]string, pids []int) {
   224  		var r int
   225  		if err := NewCall("nsFixTestNilArg").
   226  			TargetPid(pids[0]).
   227  			SpawnInNamespaces(&r); err != nil {
   228  			t.Fatalf("SpawnInNamespaces(): %v", err)
   229  		}
   230  		expectedResult := 42
   231  		if r != expectedResult {
   232  			t.Errorf("Bad result from SpawnInNamespaces(): %d instead of %d", r, expectedResult)
   233  		}
   234  	})
   235  }
   236  
   237  func TestNsFixWithNilResult(t *testing.T) {
   238  	verifyNsFix(t, func(tmpDir string, dirs map[string]string, pids []int) {
   239  		if err := NewCall("nsFixTestNilResult").
   240  			TargetPid(pids[0]).
   241  			Arg(dirs["b"]).
   242  			SpawnInNamespaces(nil); err != nil {
   243  			t.Fatalf("SpawnInNamespaces(): %v", err)
   244  		}
   245  		// dirs["a"] is bind-mounted under dirs["b"]
   246  		if _, err := os.Stat(filepath.Join(dirs["a"], "foobar")); err != nil {
   247  			t.Errorf("Stat(): %v", err)
   248  		}
   249  	})
   250  }
   251  
   252  func init() {
   253  	if switchStr := os.Getenv("TEST_NSFIX_SWITCH"); switchStr != "" {
   254  		os.Setenv("TEST_NSFIX_SWITCH", "")
   255  		parts := strings.SplitN(switchStr, ":", 2)
   256  		if len(parts) != 2 {
   257  			glog.Fatalf("bad TEST_NSFIX_SWITCH: %q", switchStr)
   258  		}
   259  		pid, err := strconv.Atoi(parts[0])
   260  		if err != nil {
   261  			glog.Fatalf("bad TEST_NSFIX_SWITCH: %q", switchStr)
   262  		}
   263  		if err := NewCall("nsFixTest2").
   264  			TargetPid(pid).
   265  			Arg(nsFixTestArg{parts[1]}).
   266  			SwitchToNamespaces(); err != nil {
   267  			glog.Fatalf("SwitchToNamespaces(): %v", err)
   268  		}
   269  	}
   270  	RegisterReexec("nsFixTest1", handleNsFixTest1, nsFixTestArg{})
   271  	RegisterReexec("nsFixTest2", handleNsFixTest2, nsFixTestArg{})
   272  	RegisterReexec("nsFixTestNilArg", handleNsFixWithNilArg, nil)
   273  	RegisterReexec("nsFixTestNilResult", handleNsFixWithNilResult, "")
   274  	if os.Getenv("TEST_NSFIX") != "" {
   275  		// NOTE: this is not a recommended way to invoke
   276  		// reexec, but may be the easiest one for testing
   277  		HandleReexec()
   278  	}
   279  }