// Copyright (c) 2021 Multus 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 server

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/containernetworking/cni/pkg/invoke"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/version"
)

// ChrootExec implements invoke.Exec to execute CNI with chroot
type ChrootExec struct {
	Stderr     io.Writer
	chrootDir  string
	workingDir string   // working directory in the outer root
	outerRoot  *os.File // outer root directory
	version.PluginDecoder
	mu sync.Mutex
}

var _ invoke.Exec = &ChrootExec{}

func (e *ChrootExec) chroot() error {
	var err error
	e.workingDir, err = os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "getwd before chroot failed: %v\n", err)
		return fmt.Errorf("getwd before chroot failed: %v", err)
	}

	e.outerRoot, err = os.Open("/")
	if err != nil {
		fmt.Fprintf(os.Stderr, "getwd before chroot failed: %v\n", err)
		return fmt.Errorf("getwd before chroot failed: %v", err)
	}

	if err := syscall.Chroot(e.chrootDir); err != nil {
		fmt.Fprintf(os.Stderr, "chroot to %s failed: %v\n", e.chrootDir, err)
		return fmt.Errorf("chroot to %s failed: %v", e.chrootDir, err)
	}

	if err := os.Chdir("/"); err != nil {
		fmt.Fprintf(os.Stderr, "chdir to \"/\" failed: %v\n", err)
		return fmt.Errorf("chdir to \"/\" failed: %v", err)
	}

	return nil
}

func (e *ChrootExec) escape() error {
	if e.outerRoot == nil || e.workingDir == "" {
		return nil
	}

	// change directory to outer root and close it
	if err := syscall.Fchdir(int(e.outerRoot.Fd())); err != nil {
		fmt.Fprintf(os.Stderr, "changing directory to outer root failed: %v\n", err)
		return fmt.Errorf("changing directory to outer root failed: %v", err)
	}

	if err := e.outerRoot.Close(); err != nil {
		fmt.Fprintf(os.Stderr, "closing outer root failed: %v\n", err)
		return fmt.Errorf("closing outer root failed: %v", err)
	}

	// chroot to current directory aka "." being the outer root
	if err := syscall.Chroot("."); err != nil {
		fmt.Fprintf(os.Stderr, "chroot to current directory failed: %v\n", err)
		return fmt.Errorf("chroot to current directory failed: %v", err)
	}

	if err := os.Chdir(e.workingDir); err != nil {
		fmt.Fprintf(os.Stderr, "chdir to working directory failed: %v\n", err)
		return fmt.Errorf("chdir to working directory failed: %v", err)
	}
	e.outerRoot = nil
	e.workingDir = ""

	return nil
}

// ExecPlugin executes CNI plugin with given environment/stdin data.
func (e *ChrootExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
	// lock and do chroot to execute plugin with host root
	e.mu.Lock()
	defer e.mu.Unlock()
	err := e.chroot()
	defer e.escape()

	if err != nil {
		fmt.Fprintf(os.Stderr, "ExecPlugin failed at chroot: %v\n", err)
		return nil, fmt.Errorf("ExecPlugin failed at chroot: %v", err)
	}

	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	c := exec.CommandContext(ctx, pluginPath)
	c.Env = environ
	c.Stdin = bytes.NewBuffer(stdinData)
	c.Stdout = stdout
	c.Stderr = stderr

	// Retry the command on "text file busy" errors
	for i := 0; i <= 5; i++ {
		err = c.Run()

		// Command succeeded
		if err == nil {
			break
		}

		// If the plugin is currently about to be written, then we wait a
		// second and try it again
		if strings.Contains(err.Error(), "text file busy") {
			time.Sleep(time.Second)
			continue
		}

		// All other errors except than the busy text file
		return nil, e.pluginErr(err, stdout.Bytes(), stderr.Bytes())
	}

	// Copy stderr to caller's buffer in case plugin printed to both
	// stdout and stderr for some reason. Ignore failures as stderr is
	// only informational.
	if e.Stderr != nil && stderr.Len() > 0 {
		_, _ = stderr.WriteTo(e.Stderr)
	}
	return stdout.Bytes(), nil
}

func (e *ChrootExec) pluginErr(err error, stdout, stderr []byte) error {
	emsg := types.Error{}
	if len(stdout) == 0 {
		if len(stderr) == 0 {
			emsg.Msg = fmt.Sprintf("netplugin failed with no error message: %v", err)
		} else {
			emsg.Msg = fmt.Sprintf("netplugin failed: %q", string(stderr))
		}
	} else if perr := json.Unmarshal(stdout, &emsg); perr != nil {
		emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(stdout), perr)
	}
	return &emsg
}

// FindInPath try to find CNI plugin based on given path
func (e *ChrootExec) FindInPath(plugin string, paths []string) (string, error) {
	e.mu.Lock()
	defer e.mu.Unlock()
	err := e.chroot()
	defer e.escape()
	if err != nil {
		fmt.Fprintf(os.Stderr, "FindInPath failed at chroot: %v\n", err)
		return "", fmt.Errorf("FindInPath failed at chroot: %v", err)
	}

	return invoke.FindInPath(plugin, paths)
}
