aboutsummaryrefslogblamecommitdiffstats
path: root/p2p/simulations/adapters/docker.go
blob: d145c46b3a5af178d6b04187d110c4bcc7a72519 (plain) (tree)

































                                                                                  



                                                                                                                                         


















                                                                             
                                        









































                                                                                  



                                                                         





















































































                                                                                                                       
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package adapters

import (
    "errors"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"

    "github.com/docker/docker/pkg/reexec"
    "github.com/ethereum/go-ethereum/node"
    "github.com/ethereum/go-ethereum/p2p/discover"
)

var (
    ErrLinuxOnly = errors.New("DockerAdapter can only be used on Linux as it uses the current binary (which must be a Linux binary)")
)

// DockerAdapter is a NodeAdapter which runs simulation nodes inside Docker
// containers.
//
// A Docker image is built which contains the current binary at /bin/p2p-node
// which when executed runs the underlying service (see the description
// of the execP2PNode function for more details)
type DockerAdapter struct {
    ExecAdapter
}

// NewDockerAdapter builds the p2p-node Docker image containing the current
// binary and returns a DockerAdapter
func NewDockerAdapter() (*DockerAdapter, error) {
    // Since Docker containers run on Linux and this adapter runs the
    // current binary in the container, it must be compiled for Linux.
    //
    // It is reasonable to require this because the caller can just
    // compile the current binary in a Docker container.
    if runtime.GOOS != "linux" {
        return nil, ErrLinuxOnly
    }

    if err := buildDockerImage(); err != nil {
        return nil, err
    }

    return &DockerAdapter{
        ExecAdapter{
            nodes: make(map[discover.NodeID]*ExecNode),
        },
    }, nil
}

// Name returns the name of the adapter for logging purposes
func (d *DockerAdapter) Name() string {
    return "docker-adapter"
}

// NewNode returns a new DockerNode using the given config
func (d *DockerAdapter) NewNode(config *NodeConfig) (Node, error) {
    if len(config.Services) == 0 {
        return nil, errors.New("node must have at least one service")
    }
    for _, service := range config.Services {
        if _, exists := serviceFuncs[service]; !exists {
            return nil, fmt.Errorf("unknown node service %q", service)
        }
    }

    // generate the config
    conf := &execNodeConfig{
        Stack: node.DefaultConfig,
        Node:  config,
    }
    conf.Stack.DataDir = "/data"
    conf.Stack.WSHost = "0.0.0.0"
    conf.Stack.WSOrigins = []string{"*"}
    conf.Stack.WSExposeAll = true
    conf.Stack.P2P.EnableMsgEvents = false
    conf.Stack.P2P.NoDiscovery = true
    conf.Stack.P2P.NAT = nil
    conf.Stack.NoUSB = true

    // listen on all interfaces on a given port, which we set when we
    // initialise NodeConfig (usually a random port)
    conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port)

    node := &DockerNode{
        ExecNode: ExecNode{
            ID:      config.ID,
            Config:  conf,
            adapter: &d.ExecAdapter,
        },
    }
    node.newCmd = node.dockerCommand
    d.ExecAdapter.nodes[node.ID] = &node.ExecNode
    return node, nil
}

// DockerNode wraps an ExecNode but exec's the current binary in a docker
// container rather than locally
type DockerNode struct {
    ExecNode
}

// dockerCommand returns a command which exec's the binary in a Docker
// container.
//
// It uses a shell so that we can pass the _P2P_NODE_CONFIG environment
// variable to the container using the --env flag.
func (n *DockerNode) dockerCommand() *exec.Cmd {
    return exec.Command(
        "sh", "-c",
        fmt.Sprintf(
            `exec docker run --interactive --env _P2P_NODE_CONFIG="${_P2P_NODE_CONFIG}" %s p2p-node %s %s`,
            dockerImage, strings.Join(n.Config.Node.Services, ","), n.ID.String(),
        ),
    )
}

// dockerImage is the name of the Docker image which gets built to run the
// simulation node
const dockerImage = "p2p-node"

// buildDockerImage builds the Docker image which is used to run the simulation
// node in a Docker container.
//
// It adds the current binary as "p2p-node" so that it runs execP2PNode
// when executed.
func buildDockerImage() error {
    // create a directory to use as the build context
    dir, err := ioutil.TempDir("", "p2p-docker")
    if err != nil {
        return err
    }
    defer os.RemoveAll(dir)

    // copy the current binary into the build context
    bin, err := os.Open(reexec.Self())
    if err != nil {
        return err
    }
    defer bin.Close()
    dst, err := os.OpenFile(filepath.Join(dir, "self.bin"), os.O_WRONLY|os.O_CREATE, 0755)
    if err != nil {
        return err
    }
    defer dst.Close()
    if _, err := io.Copy(dst, bin); err != nil {
        return err
    }

    // create the Dockerfile
    dockerfile := []byte(`
FROM ubuntu:16.04
RUN mkdir /data
ADD self.bin /bin/p2p-node
    `)
    if err := ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile, 0644); err != nil {
        return err
    }

    // run 'docker build'
    cmd := exec.Command("docker", "build", "-t", dockerImage, dir)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("error building docker image: %s", err)
    }

    return nil
}