github.com/klaytn/klaytn@v1.12.1/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  	"os"
    28  	"os/exec"
    29  	"path/filepath"
    30  	"runtime"
    31  	"strings"
    32  
    33  	"github.com/docker/docker/pkg/reexec"
    34  	"github.com/klaytn/klaytn/networks/p2p/discover"
    35  	"github.com/klaytn/klaytn/node"
    36  )
    37  
    38  var ErrLinuxOnly = errors.New("DockerAdapter can only be used on Linux as it uses the current binary (which must be a Linux binary)")
    39  
    40  // DockerAdapter is a NodeAdapter which runs simulation nodes inside Docker
    41  // containers.
    42  //
    43  // A Docker image is built which contains the current binary at /bin/p2p-node
    44  // which when executed runs the underlying service (see the description
    45  // of the execP2PNode function for more details)
    46  type DockerAdapter struct {
    47  	ExecAdapter
    48  }
    49  
    50  // NewDockerAdapter builds the p2p-node Docker image containing the current
    51  // binary and returns a DockerAdapter
    52  func NewDockerAdapter() (*DockerAdapter, error) {
    53  	// Since Docker containers run on Linux and this adapter runs the
    54  	// current binary in the container, it must be compiled for Linux.
    55  	//
    56  	// It is reasonable to require this because the caller can just
    57  	// compile the current binary in a Docker container.
    58  	if runtime.GOOS != "linux" {
    59  		return nil, ErrLinuxOnly
    60  	}
    61  
    62  	if err := buildDockerImage(); err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	return &DockerAdapter{
    67  		ExecAdapter{
    68  			nodes: make(map[discover.NodeID]*ExecNode),
    69  		},
    70  	}, nil
    71  }
    72  
    73  // Name returns the name of the adapter for logging purposes
    74  func (d *DockerAdapter) Name() string {
    75  	return "docker-adapter"
    76  }
    77  
    78  // NewNode returns a new DockerNode using the given config
    79  func (d *DockerAdapter) NewNode(config *NodeConfig) (Node, error) {
    80  	if len(config.Services) == 0 {
    81  		return nil, errors.New("node must have at least one service")
    82  	}
    83  	for _, service := range config.Services {
    84  		if _, exists := serviceFuncs[service]; !exists {
    85  			return nil, fmt.Errorf("unknown node service %q", service)
    86  		}
    87  	}
    88  
    89  	// generate the config
    90  	conf := &execNodeConfig{
    91  		Stack: node.DefaultConfig,
    92  		Node:  config,
    93  	}
    94  	conf.Stack.DataDir = "/data"
    95  	conf.Stack.WSHost = "0.0.0.0"
    96  	conf.Stack.WSOrigins = []string{"*"}
    97  	conf.Stack.WSExposeAll = true
    98  	conf.Stack.P2P.EnableMsgEvents = false
    99  	conf.Stack.P2P.NoDiscovery = true
   100  	conf.Stack.P2P.NAT = nil
   101  
   102  	// listen on all interfaces on a given port, which we set when we
   103  	// initialise NodeConfig (usually a random port)
   104  	conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port)
   105  
   106  	node := &DockerNode{
   107  		ExecNode: ExecNode{
   108  			ID:      config.ID,
   109  			Config:  conf,
   110  			adapter: &d.ExecAdapter,
   111  		},
   112  	}
   113  	node.newCmd = node.dockerCommand
   114  	d.ExecAdapter.nodes[node.ID] = &node.ExecNode
   115  	return node, nil
   116  }
   117  
   118  // DockerNode wraps an ExecNode but exec's the current binary in a docker
   119  // container rather than locally
   120  type DockerNode struct {
   121  	ExecNode
   122  }
   123  
   124  // dockerCommand returns a command which exec's the binary in a Docker
   125  // container.
   126  //
   127  // It uses a shell so that we can pass the _P2P_NODE_CONFIG environment
   128  // variable to the container using the --env flag.
   129  func (n *DockerNode) dockerCommand() *exec.Cmd {
   130  	return exec.Command(
   131  		"sh", "-c",
   132  		fmt.Sprintf(
   133  			`exec docker run --interactive --env _P2P_NODE_CONFIG="${_P2P_NODE_CONFIG}" %s p2p-node %s %s`,
   134  			dockerImage, strings.Join(n.Config.Node.Services, ","), n.ID.String(),
   135  		),
   136  	)
   137  }
   138  
   139  // dockerImage is the name of the Docker image which gets built to run the
   140  // simulation node
   141  const dockerImage = "p2p-node"
   142  
   143  // buildDockerImage builds the Docker image which is used to run the simulation
   144  // node in a Docker container.
   145  //
   146  // It adds the current binary as "p2p-node" so that it runs execP2PNode
   147  // when executed.
   148  func buildDockerImage() error {
   149  	// create a directory to use as the build context
   150  	dir, err := os.MkdirTemp("", "p2p-docker")
   151  	if err != nil {
   152  		return err
   153  	}
   154  	defer os.RemoveAll(dir)
   155  
   156  	// copy the current binary into the build context
   157  	bin, err := os.Open(reexec.Self())
   158  	if err != nil {
   159  		return err
   160  	}
   161  	defer bin.Close()
   162  	dst, err := os.OpenFile(filepath.Join(dir, "self.bin"), os.O_WRONLY|os.O_CREATE, 0o755)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	defer dst.Close()
   167  	if _, err := io.Copy(dst, bin); err != nil {
   168  		return err
   169  	}
   170  
   171  	// create the Dockerfile
   172  	dockerfile := []byte(`
   173  FROM ubuntu:16.04
   174  RUN mkdir /data
   175  ADD self.bin /bin/p2p-node
   176  	`)
   177  	if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile, 0o644); err != nil {
   178  		return err
   179  	}
   180  
   181  	// run 'docker build'
   182  	cmd := exec.Command("docker", "build", "-t", dockerImage, dir)
   183  	cmd.Stdout = os.Stdout
   184  	cmd.Stderr = os.Stderr
   185  	if err := cmd.Run(); err != nil {
   186  		return fmt.Errorf("error building docker image: %s", err)
   187  	}
   188  
   189  	return nil
   190  }