// Copyright 2018 The gVisor Authors.
//
// 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 root

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"gvisor.dev/gvisor/pkg/cleanup"
	"gvisor.dev/gvisor/pkg/test/criutil"
	"gvisor.dev/gvisor/pkg/test/dockerutil"
	"gvisor.dev/gvisor/pkg/test/testutil"
)

// Tests for crictl have to be run as root (rather than in a user namespace)
// because crictl creates named network namespaces in /var/run/netns/.

// Sandbox returns a JSON config for a simple sandbox. Sandbox names must be
// unique so different names should be used when running tests on the same
// containerd instance.
func Sandbox(name string) string {
	// Sandbox is a default JSON config for a sandbox.
	s := map[string]interface{}{
		"metadata": map[string]string{
			"name":      name,
			"namespace": "default",
			"uid":       testutil.RandomID(""),
		},
		"linux":         map[string]string{},
		"log_directory": "/tmp",
	}

	v, err := json.Marshal(s)
	if err != nil {
		// This shouldn't happen.
		panic(err)
	}
	return string(v)
}

// SimpleSpec returns a JSON config for a simple container that runs the
// specified command in the specified image.
func SimpleSpec(name, image string, cmd []string, extra map[string]interface{}) string {
	s := map[string]interface{}{
		"metadata": map[string]string{
			"name": name,
		},
		"image": map[string]string{
			"image": testutil.ImageByName(image),
		},
		// Log files are not deleted after root tests are run. Log to random
		// paths to ensure logs are fresh.
		"log_path": fmt.Sprintf("%s.log", testutil.RandomID(name)),
		"stdin":    false,
		"tty":      false,
	}
	if len(cmd) > 0 { // Omit if empty.
		s["command"] = cmd
	}
	for k, v := range extra {
		s[k] = v // Extra settings.
	}
	v, err := json.Marshal(s)
	if err != nil {
		// This shouldn't happen.
		panic(err)
	}
	return string(v)
}

// Httpd is a JSON config for an httpd container.
var Httpd = SimpleSpec("httpd", "basic/httpd", nil, nil)

// TestCrictlSanity refers to b/112433158.
func TestCrictlSanity(t *testing.T) {
	for _, version := range allVersions {
		t.Run(version, func(t *testing.T) {
			// Setup containerd and crictl.
			crictl, cleanup, err := setup(t, version)
			if err != nil {
				t.Fatalf("failed to setup crictl: %v", err)
			}
			defer cleanup()
			podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), Httpd)
			if err != nil {
				t.Fatalf("start failed: %v", err)
			}

			// Look for the httpd page.
			if err = httpGet(crictl, podID, "index.html"); err != nil {
				t.Fatalf("failed to get page: %v", err)
			}

			// Stop everything.
			if err := crictl.StopPodAndContainer(podID, contID); err != nil {
				t.Fatalf("stop failed: %v", err)
			}
		})
	}
}

// HttpdMountPaths is a JSON config for an httpd container with additional
// mounts.
var HttpdMountPaths = SimpleSpec("httpd", "basic/httpd", nil, map[string]interface{}{
	"mounts": []map[string]interface{}{
		map[string]interface{}{
			"container_path": "/var/run/secrets/kubernetes.io/serviceaccount",
			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/volumes/kubernetes.io~secret/default-token-2rpfx",
			"readonly":       true,
		},
		map[string]interface{}{
			"container_path": "/etc/hosts",
			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/etc-hosts",
			"readonly":       false,
		},
		map[string]interface{}{
			"container_path": "/dev/termination-log",
			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/containers/httpd/d1709580",
			"readonly":       false,
		},
		map[string]interface{}{
			"container_path": "/usr/local/apache2/htdocs/test",
			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064",
			"readonly":       true,
		},
	},
	"linux": map[string]interface{}{},
})

// TestMountPaths refers to b/117635704.
func TestMountPaths(t *testing.T) {
	for _, version := range allVersions {
		t.Run(version, func(t *testing.T) {
			// Setup containerd and crictl.
			crictl, cleanup, err := setup(t, version)
			if err != nil {
				t.Fatalf("failed to setup crictl: %v", err)
			}
			defer cleanup()
			podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), HttpdMountPaths)
			if err != nil {
				t.Fatalf("start failed: %v", err)
			}

			// Look for the directory available at /test.
			if err = httpGet(crictl, podID, "test"); err != nil {
				t.Fatalf("failed to get page: %v", err)
			}

			// Stop everything.
			if err := crictl.StopPodAndContainer(podID, contID); err != nil {
				t.Fatalf("stop failed: %v", err)
			}
		})
	}
}

// TestMountPaths refers to b/118728671.
func TestMountOverSymlinks(t *testing.T) {
	for _, version := range allVersions {
		t.Run(version, func(t *testing.T) {
			// Setup containerd and crictl.
			crictl, cleanup, err := setup(t, version)
			if err != nil {
				t.Fatalf("failed to setup crictl: %v", err)
			}
			defer cleanup()

			spec := SimpleSpec("busybox", "basic/resolv", []string{"sleep", "1000"}, nil)
			podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/resolv", Sandbox("default"), spec)
			if err != nil {
				t.Fatalf("start failed: %v", err)
			}

			out, err := crictl.Exec(contID, "readlink", "/etc/resolv.conf")
			if err != nil {
				t.Fatalf("readlink failed: %v, out: %s", err, out)
			}
			if want := "/tmp/resolv.conf"; !strings.Contains(string(out), want) {
				t.Fatalf("/etc/resolv.conf is not pointing to %q: %q", want, string(out))
			}

			etc, err := crictl.Exec(contID, "cat", "/etc/resolv.conf")
			if err != nil {
				t.Fatalf("cat failed: %v, out: %s", err, etc)
			}
			tmp, err := crictl.Exec(contID, "cat", "/tmp/resolv.conf")
			if err != nil {
				t.Fatalf("cat failed: %v, out: %s", err, out)
			}
			if tmp != etc {
				t.Fatalf("file content doesn't match:\n\t/etc/resolv.conf: %s\n\t/tmp/resolv.conf: %s", string(etc), string(tmp))
			}

			// Stop everything.
			if err := crictl.StopPodAndContainer(podID, contID); err != nil {
				t.Fatalf("stop failed: %v", err)
			}
		})
	}
}

// TestHomeDir tests that the HOME environment variable is set for
// Pod containers.
func TestHomeDir(t *testing.T) {
	for _, version := range allVersions {
		t.Run(version, func(t *testing.T) {
			// Setup containerd and crictl.
			crictl, cleanup, err := setup(t, version)
			if err != nil {
				t.Fatalf("failed to setup crictl: %v", err)
			}
			defer cleanup()

			// Note that container ID returned here is a sub-container. All Pod
			// containers are sub-containers. The root container of the sandbox is the
			// pause container.
			t.Run("sub-container", func(t *testing.T) {
				contSpec := SimpleSpec("subcontainer", "basic/busybox", []string{"sh", "-c", "echo $HOME"}, nil)
				podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("subcont-sandbox"), contSpec)
				if err != nil {
					t.Fatalf("start failed: %v", err)
				}

				out, err := crictl.Logs(contID)
				if err != nil {
					t.Fatalf("failed retrieving container logs: %v, out: %s", err, out)
				}
				if got, want := strings.TrimSpace(string(out)), "/root"; got != want {
					t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want)
				}

				// Stop everything; note that the pod may have already stopped.
				crictl.StopPodAndContainer(podID, contID)
			})

			// Tests that HOME is set for the exec process.
			t.Run("exec", func(t *testing.T) {
				contSpec := SimpleSpec("exec", "basic/busybox", []string{"sleep", "1000"}, nil)
				podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("exec-sandbox"), contSpec)
				if err != nil {
					t.Fatalf("start failed: %v", err)
				}

				out, err := crictl.Exec(contID, "sh", "-c", "echo $HOME")
				if err != nil {
					t.Fatalf("failed retrieving container logs: %v, out: %s", err, out)
				}
				if got, want := strings.TrimSpace(string(out)), "/root"; got != want {
					t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want)
				}

				// Stop everything.
				if err := crictl.StopPodAndContainer(podID, contID); err != nil {
					t.Fatalf("stop failed: %v", err)
				}
			})
		})
	}
}

const containerdRuntime = "runsc"

const v1Template = `
disabled_plugins = ["restart"]
[plugins.cri]
  disable_tcp_service = true
[plugins.linux]
  shim = "%s"
  shim_debug = true
[plugins.cri.containerd.runtimes.` + containerdRuntime + `]
  runtime_type = "io.containerd.runtime.v1.linux"
  runtime_engine = "%s"
  runtime_root = "%s/root/runsc"
`

const v2Template = `
disabled_plugins = ["restart"]
[plugins.cri]
  disable_tcp_service = true
[plugins.linux]
  shim_debug = true
[plugins.cri.containerd.runtimes.` + containerdRuntime + `]
  runtime_type = "io.containerd.` + containerdRuntime + `.v1"
[plugins.cri.containerd.runtimes.` + containerdRuntime + `.options]
  TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options"
`

const (
	// v1 is the containerd API v1.
	v1 string = "v1"

	// v1 is the containerd API v21.
	v2 string = "v2"
)

// allVersions is the set of known versions.
var allVersions = []string{v1, v2}

// setup sets up before a test. Specifically it:
// * Creates directories and a socket for containerd to utilize.
// * Runs containerd and waits for it to reach a "ready" state for testing.
// * Returns a cleanup function that should be called at the end of the test.
func setup(t *testing.T, version string) (*criutil.Crictl, func(), error) {
	// Create temporary containerd root and state directories, and a socket
	// via which crictl and containerd communicate.
	containerdRoot, err := ioutil.TempDir(testutil.TmpDir(), "containerd-root")
	if err != nil {
		t.Fatalf("failed to create containerd root: %v", err)
	}
	cu := cleanup.Make(func() { os.RemoveAll(containerdRoot) })
	defer cu.Clean()
	t.Logf("Using containerd root: %s", containerdRoot)

	containerdState, err := ioutil.TempDir(testutil.TmpDir(), "containerd-state")
	if err != nil {
		t.Fatalf("failed to create containerd state: %v", err)
	}
	cu.Add(func() { os.RemoveAll(containerdState) })
	t.Logf("Using containerd state: %s", containerdState)

	sockDir, err := ioutil.TempDir(testutil.TmpDir(), "containerd-sock")
	if err != nil {
		t.Fatalf("failed to create containerd socket directory: %v", err)
	}
	cu.Add(func() { os.RemoveAll(sockDir) })
	sockAddr := path.Join(sockDir, "test.sock")
	t.Logf("Using containerd socket: %s", sockAddr)

	// Extract the containerd version.
	versionCmd := exec.Command(getContainerd(), "-v")
	out, err := versionCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("error extracting containerd version: %v (%s)", err, string(out))
	}
	r := regexp.MustCompile(" v([0-9]+)\\.([0-9]+)\\.([0-9+])")
	vs := r.FindStringSubmatch(string(out))
	if len(vs) != 4 {
		t.Fatalf("error unexpected version string: %s", string(out))
	}
	major, err := strconv.ParseUint(vs[1], 10, 64)
	if err != nil {
		t.Fatalf("error parsing containerd major version: %v (%s)", err, string(out))
	}
	minor, err := strconv.ParseUint(vs[2], 10, 64)
	if err != nil {
		t.Fatalf("error parsing containerd minor version: %v (%s)", err, string(out))
	}
	t.Logf("Using containerd version: %d.%d", major, minor)

	// We rewrite a configuration. This is based on the current docker
	// configuration for the runtime under test.
	runtime, err := dockerutil.RuntimePath()
	if err != nil {
		t.Fatalf("error discovering runtime path: %v", err)
	}
	t.Logf("Using runtime: %v", runtime)

	// Construct a PATH that includes the runtime directory. This is
	// because the shims will be installed there, and containerd may infer
	// the binary name and search the PATH.
	runtimeDir := path.Dir(runtime)
	modifiedPath := os.Getenv("PATH")
	if modifiedPath != "" {
		modifiedPath = ":" + modifiedPath // We prepend below.
	}
	modifiedPath = path.Dir(getContainerd()) + modifiedPath
	modifiedPath = runtimeDir + ":" + modifiedPath
	t.Logf("Using PATH: %v", modifiedPath)

	var (
		config   string
		runpArgs []string
	)
	switch version {
	case v1:
		// This is only supported less than 1.3.
		if major > 1 || (major == 1 && minor >= 3) {
			t.Skipf("skipping unsupported containerd (want less than 1.3, got %d.%d)", major, minor)
		}

		// We provide the shim, followed by the runtime, and then a
		// temporary root directory.
		config = fmt.Sprintf(v1Template, criutil.ResolvePath("gvisor-containerd-shim"), runtime, containerdRoot)
	case v2:
		// This is only supported past 1.2.
		if major < 1 || (major == 1 && minor <= 1) {
			t.Skipf("skipping incompatible containerd (want at least 1.2, got %d.%d)", major, minor)
		}

		// The runtime is provided via parameter. Note that the v2 shim
		// binary name is always containerd-shim-* so we don't actually
		// care about the docker runtime name.
		config = v2Template
	default:
		t.Fatalf("unknown version: %d", version)
	}
	t.Logf("Using config: %s", config)

	// Generate the configuration for the test.
	configFile, configCleanup, err := testutil.WriteTmpFile("containerd-config", config)
	if err != nil {
		t.Fatalf("failed to write containerd config")
	}
	cu.Add(configCleanup)

	// Start containerd.
	args := []string{
		getContainerd(),
		"--config", configFile,
		"--log-level", "debug",
		"--root", containerdRoot,
		"--state", containerdState,
		"--address", sockAddr,
	}
	t.Logf("Using args: %s", strings.Join(args, " "))
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Env = append(os.Environ(), "PATH="+modifiedPath)

	// Include output in logs.
	stderrPipe, err := cmd.StderrPipe()
	if err != nil {
		t.Fatalf("failed to create stderr pipe: %v", err)
	}
	cu.Add(func() { stderrPipe.Close() })
	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		t.Fatalf("failed to create stdout pipe: %v", err)
	}
	cu.Add(func() { stdoutPipe.Close() })
	var (
		wg     sync.WaitGroup
		stderr bytes.Buffer
		stdout bytes.Buffer
	)
	startupR, startupW := io.Pipe()
	wg.Add(2)
	go func() {
		defer wg.Done()
		io.Copy(io.MultiWriter(startupW, &stderr), stderrPipe)
	}()
	go func() {
		defer wg.Done()
		io.Copy(io.MultiWriter(startupW, &stdout), stdoutPipe)
	}()
	cu.Add(func() {
		wg.Wait()
		t.Logf("containerd stdout: %s", stdout.String())
		t.Logf("containerd stderr: %s", stderr.String())
	})

	// Start the process.
	if err := cmd.Start(); err != nil {
		t.Fatalf("failed running containerd: %v", err)
	}

	// Wait for containerd to boot.
	if err := testutil.WaitUntilRead(startupR, "Start streaming server", nil, 10*time.Second); err != nil {
		t.Fatalf("failed to start containerd: %v", err)
	}

	// Discard all subsequent data.
	go io.Copy(ioutil.Discard, startupR)

	// Create the crictl interface.
	cc := criutil.NewCrictl(t, sockAddr, runpArgs)
	cu.Add(cc.CleanUp)

	// Kill must be the last cleanup (as it will be executed first).
	cu.Add(func() {
		// Best effort: ignore errors.
		testutil.KillCommand(cmd)
	})

	return cc, cu.Release(), nil
}

// httpGet GETs the contents of a file served from a pod on port 80.
func httpGet(crictl *criutil.Crictl, podID, filePath string) error {
	// Get the IP of the httpd server.
	ip, err := crictl.PodIP(podID)
	if err != nil {
		return fmt.Errorf("failed to get IP from pod %q: %v", podID, err)
	}

	// GET the page. We may be waiting for the server to start, so retry
	// with a timeout.
	var resp *http.Response
	cb := func() error {
		r, err := http.Get(fmt.Sprintf("http://%s", path.Join(ip, filePath)))
		resp = r
		return err
	}
	if err := testutil.Poll(cb, 20*time.Second); err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("bad status returned: %d", resp.StatusCode)
	}
	return nil
}

func getContainerd() string {
	// Use the local path if it exists, otherwise, use the system one.
	if _, err := os.Stat("/usr/local/bin/containerd"); err == nil {
		return "/usr/local/bin/containerd"
	}
	return "/usr/bin/containerd"
}