github.com/klaytn/klaytn@v1.10.2/networks/p2p/simulations/adapters/docker.go (about)

     1  // Modifications Copyright 2018 The klaytn Authors
     2  // Copyright 2017 The go-ethereum Authors
     3  // This file is part of the go-ethereum library.
     4  //
     5  // The go-ethereum library is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Lesser General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // The go-ethereum library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU Lesser General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public License
    16  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    17  //
    18  // This file is derived from p2p/simulations/adapters/docker.go (2018/06/04).
    19  // Modified and improved for the klaytn development.
    20  
    21  package adapters
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"runtime"
    32  	"strings"
    33  
    34  	"github.com/docker/docker/pkg/reexec"
    35  	"github.com/klaytn/klaytn/networks/p2p/discover"
    36  	"github.com/klaytn/klaytn/node"
    37  )
    38  
    39  var ErrLinuxOnly = errors.New("DockerAdapter can only be used on Linux as it uses the current binary (which must be a Linux binary)")
    40  
    41  // DockerAdapter is a NodeAdapter which runs simulation nodes inside Docker
    42  // containers.
    43  //
    44  // A Docker image is built which contains the current binary at /bin/p2p-node
    45  // which when executed runs the underlying service (see the description
    46  // of the execP2PNode function for more details)
    47  type DockerAdapter struct {
    48  	ExecAdapter
    49  }
    50  
    51  // NewDockerAdapter builds the p2p-node Docker image containing the current
    52  // binary and returns a DockerAdapter
    53  func NewDockerAdapter() (*DockerAdapter, error) {
    54  	// Since Docker containers run on Linux and this adapter runs the
    55  	// current binary in the container, it must be compiled for Linux.
    56  	//
    57  	// It is reasonable to require this because the caller can just
    58  	// compile the current binary in a Docker container.
    59  	if runtime.GOOS != "linux" {
    60  		return nil, ErrLinuxOnly
    61  	}
    62  
    63  	if err := buildDockerImage(); err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	return &DockerAdapter{
    68  		ExecAdapter{
    69  			nodes: make(map[discover.NodeID]*ExecNode),
    70  		},
    71  	}, nil
    72  }
    73  
    74  // Name returns the name of the adapter for logging purposes
    75  func (d *DockerAdapter) Name() string {
    76  	return "docker-adapter"
    77  }
    78  
    79  // NewNode returns a new DockerNode using the given config
    80  func (d *DockerAdapter) NewNode(config *NodeConfig) (Node, error) {
    81  	if len(config.Services) == 0 {
    82  		return nil, errors.New("node must have at least one service")
    83  	}
    84  	for _, service := range config.Services {
    85  		if _, exists := serviceFuncs[service]; !exists {
    86  			return nil, fmt.Errorf("unknown node service %q", service)
    87  		}
    88  	}
    89  
    90  	// generate the config
    91  	conf := &execNodeConfig{
    92  		Stack: node.DefaultConfig,
    93  		Node:  config,
    94  	}
    95  	conf.Stack.DataDir = "/data"
    96  	conf.Stack.WSHost = "0.0.0.0"
    97  	conf.Stack.WSOrigins = []string{"*"}
    98  	conf.Stack.WSExposeAll = true
    99  	conf.Stack.P2P.EnableMsgEvents = false
   100  	conf.Stack.P2P.NoDiscovery = true
   101  	conf.Stack.P2P.NAT = nil
   102  
   103  	// listen on all interfaces on a given port, which we set when we
   104  	// initialise NodeConfig (usually a random port)
   105  	conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port)
   106  
   107  	node := &DockerNode{
   108  		ExecNode: ExecNode{
   109  			ID:      config.ID,
   110  			Config:  conf,
   111  			adapter: &d.ExecAdapter,
   112  		},
   113  	}
   114  	node.newCmd = node.dockerCommand
   115  	d.ExecAdapter.nodes[node.ID] = &node.ExecNode
   116  	return node, nil
   117  }
   118  
   119  // DockerNode wraps an ExecNode but exec's the current binary in a docker
   120  // container rather than locally
   121  type DockerNode struct {
   122  	ExecNode
   123  }
   124  
   125  // dockerCommand returns a command which exec's the binary in a Docker
   126  // container.
   127  //
   128  // It uses a shell so that we can pass the _P2P_NODE_CONFIG environment
   129  // variable to the container using the --env flag.
   130  func (n *DockerNode) dockerCommand() *exec.Cmd {
   131  	return exec.Command(
   132  		"sh", "-c",
   133  		fmt.Sprintf(
   134  			`exec docker run --interactive --env _P2P_NODE_CONFIG="${_P2P_NODE_CONFIG}" %s p2p-node %s %s`,
   135  			dockerImage, strings.Join(n.Config.Node.Services, ","), n.ID.String(),
   136  		),
   137  	)
   138  }
   139  
   140  // dockerImage is the name of the Docker image which gets built to run the
   141  // simulation node
   142  const dockerImage = "p2p-node"
   143  
   144  // buildDockerImage builds the Docker image which is used to run the simulation
   145  // node in a Docker container.
   146  //
   147  // It adds the current binary as "p2p-node" so that it runs execP2PNode
   148  // when executed.
   149  func buildDockerImage() error {
   150  	// create a directory to use as the build context
   151  	dir, err := ioutil.TempDir("", "p2p-docker")
   152  	if err != nil {
   153  		return err
   154  	}
   155  	defer os.RemoveAll(dir)
   156  
   157  	// copy the current binary into the build context
   158  	bin, err := os.Open(reexec.Self())
   159  	if err != nil {
   160  		return err
   161  	}
   162  	defer bin.Close()
   163  	dst, err := os.OpenFile(filepath.Join(dir, "self.bin"), os.O_WRONLY|os.O_CREATE, 0o755)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	defer dst.Close()
   168  	if _, err := io.Copy(dst, bin); err != nil {
   169  		return err
   170  	}
   171  
   172  	// create the Dockerfile
   173  	dockerfile := []byte(`
   174  FROM ubuntu:16.04
   175  RUN mkdir /data
   176  ADD self.bin /bin/p2p-node
   177  	`)
   178  	if err := ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile, 0o644); err != nil {
   179  		return err
   180  	}
   181  
   182  	// run 'docker build'
   183  	cmd := exec.Command("docker", "build", "-t", dockerImage, dir)
   184  	cmd.Stdout = os.Stdout
   185  	cmd.Stderr = os.Stderr
   186  	if err := cmd.Run(); err != nil {
   187  		return fmt.Errorf("error building docker image: %s", err)
   188  	}
   189  
   190  	return nil
   191  }