github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/executor/docker/network.go (about)

     1  package docker
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"time"
     9  
    10  	"github.com/docker/docker/api/types"
    11  	"github.com/docker/docker/api/types/container"
    12  	"github.com/filecoin-project/bacalhau/pkg/logger"
    13  	"github.com/filecoin-project/bacalhau/pkg/model"
    14  	"github.com/pkg/errors"
    15  	"github.com/rs/zerolog/log"
    16  	"go.uber.org/multierr"
    17  )
    18  
    19  const (
    20  	dockerNetworkNone   = container.NetworkMode("none")
    21  	dockerNetworkHost   = container.NetworkMode("host")
    22  	dockerNetworkBridge = container.NetworkMode("bridge")
    23  )
    24  
    25  const (
    26  	// The Docker image used to provide HTTP filtering and throttling. See
    27  	// pkg/executor/docker/gateway/Dockerfile for design notes. We specify this
    28  	// using a fully-versioned tag so that the interface between code and image
    29  	// stay in sync.
    30  	httpGatewayImage = "ghcr.io/bacalhau-project/http-gateway:v0.3.17"
    31  
    32  	// The hostname used by Mac OS X and Windows hosts to refer to the Docker
    33  	// host in a network context. Linux hosts can use this hostname if they
    34  	// are set up using the `dockerHostAddCommand` as an extra host.
    35  	dockerHostHostname = "host.docker.internal"
    36  
    37  	// The magic word recognized by the Docker engine in place of an IP address
    38  	// that always maps to the IP address of the Docker host.
    39  	dockerHostIPAddressMagicWord = "host-gateway"
    40  
    41  	// A string that can be passed as an ExtraHost to a Docker run or create
    42  	// command that will ensure the host is visible on the network from within
    43  	// the container, even on a Linux host where localhost is sufficient.
    44  	dockerHostAddCommand = dockerHostHostname + ":" + dockerHostIPAddressMagicWord
    45  
    46  	// This time should match the --interval= option specified on the container
    47  	// HEALTHCHECK (as the health status only updates this frequently so more
    48  	// frequent calls are useless)
    49  	httpGatewayHealthcheckInterval = time.Second
    50  
    51  	// The port used by the proxy server within the HTTP gateway container. This
    52  	// is also specified in squid.conf and gateway.sh.
    53  	httpProxyPort = 8080
    54  )
    55  
    56  var (
    57  	// The capabilities that the gateway container needs. See the Dockerfile.
    58  	gatewayCapabilities = []string{"NET_ADMIN"}
    59  )
    60  
    61  func (e *Executor) setupNetworkForJob(
    62  	ctx context.Context,
    63  	shard model.JobShard,
    64  	containerConfig *container.Config,
    65  	hostConfig *container.HostConfig,
    66  ) (err error) {
    67  	containerConfig.NetworkDisabled = shard.Job.Spec.Network.Disabled()
    68  	switch shard.Job.Spec.Network.Type {
    69  	case model.NetworkNone:
    70  		hostConfig.NetworkMode = dockerNetworkNone
    71  	case model.NetworkFull:
    72  		hostConfig.NetworkMode = dockerNetworkHost
    73  		hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, dockerHostAddCommand)
    74  	case model.NetworkHTTP:
    75  		var internalNetwork *types.NetworkResource
    76  		var proxyAddr *net.TCPAddr
    77  		internalNetwork, proxyAddr, err = e.createHTTPGateway(ctx, shard)
    78  		if err != nil {
    79  			return
    80  		}
    81  		hostConfig.NetworkMode = container.NetworkMode(internalNetwork.Name)
    82  		containerConfig.Env = append(containerConfig.Env,
    83  			fmt.Sprintf("http_proxy=%s", proxyAddr.String()),
    84  			fmt.Sprintf("https_proxy=%s", proxyAddr.String()),
    85  		)
    86  	default:
    87  		err = fmt.Errorf("unsupported network type %q", shard.Job.Spec.Network.Type.String())
    88  	}
    89  	return
    90  }
    91  
    92  func (e *Executor) createHTTPGateway(
    93  	ctx context.Context,
    94  	shard model.JobShard,
    95  ) (*types.NetworkResource, *net.TCPAddr, error) {
    96  	// Get the gateway image if we don't have it already
    97  	err := e.client.PullImage(ctx, httpGatewayImage)
    98  	if err != nil {
    99  		return nil, nil, errors.Wrap(err, "error pulling gateway image")
   100  	}
   101  
   102  	// Create an internal only bridge network to join our gateway and job container
   103  	networkResp, err := e.client.NetworkCreate(ctx, e.dockerObjectName(shard, "network"), types.NetworkCreate{
   104  		Driver:     "bridge",
   105  		Scope:      "local",
   106  		Internal:   true,
   107  		Attachable: true,
   108  		Labels:     e.jobContainerLabels(shard),
   109  	})
   110  	if err != nil {
   111  		return nil, nil, errors.Wrap(err, "error creating network")
   112  	}
   113  
   114  	// Get the subnet that Docker has picked for the newly created network
   115  	internalNetwork, err := e.client.NetworkInspect(ctx, networkResp.ID, types.NetworkInspectOptions{})
   116  	if err != nil || len(internalNetwork.IPAM.Config) < 1 {
   117  		return nil, nil, errors.Wrap(err, "error getting network subnet")
   118  	}
   119  	subnet := internalNetwork.IPAM.Config[0].Subnet
   120  
   121  	// Create the gateway container initially attached to the *host* network
   122  	domainList, derr := json.Marshal(shard.Job.Spec.Network.DomainSet())
   123  	clientList, cerr := json.Marshal([]string{subnet})
   124  	if derr != nil || cerr != nil {
   125  		return nil, nil, errors.Wrap(multierr.Combine(derr, cerr), "error preparing gateway config")
   126  	}
   127  
   128  	gatewayContainer, err := e.client.ContainerCreate(ctx, &container.Config{
   129  		Image: httpGatewayImage,
   130  		Env: []string{
   131  			fmt.Sprintf("BACALHAU_HTTP_CLIENTS=%s", clientList),
   132  			fmt.Sprintf("BACALHAU_HTTP_DOMAINS=%s", domainList),
   133  			fmt.Sprintf("BACALHAU_JOB_ID=%s", shard.Job.Metadata.ID),
   134  		},
   135  		Healthcheck:     &container.HealthConfig{}, //TODO
   136  		NetworkDisabled: false,
   137  		Labels:          e.jobContainerLabels(shard),
   138  	}, &container.HostConfig{
   139  		NetworkMode: dockerNetworkBridge,
   140  		CapAdd:      gatewayCapabilities,
   141  		ExtraHosts:  []string{dockerHostAddCommand},
   142  	}, nil, nil, e.dockerObjectName(shard, "gateway"))
   143  	if err != nil {
   144  		return nil, nil, errors.Wrap(err, "error creating gateway container")
   145  	}
   146  
   147  	// Attach the bridge network to the container
   148  	err = e.client.NetworkConnect(ctx, internalNetwork.ID, gatewayContainer.ID, nil)
   149  	if err != nil {
   150  		return nil, nil, errors.Wrap(err, "error attaching network to gateway")
   151  	}
   152  
   153  	// Start the container and wait for it to come up
   154  	err = e.client.ContainerStart(ctx, gatewayContainer.ID, types.ContainerStartOptions{})
   155  	if err != nil {
   156  		return nil, nil, errors.Wrap(err, "failed to start network gateway container")
   157  	}
   158  
   159  	stdout, stderr, err := e.client.FollowLogs(ctx, gatewayContainer.ID)
   160  	if err != nil {
   161  		return nil, nil, errors.Wrap(err, "failed to get gateway container logs")
   162  	}
   163  	go logger.LogStream(log.Ctx(ctx).With().Str("Source", "stdout").Logger().WithContext(ctx), stdout)
   164  	go logger.LogStream(log.Ctx(ctx).With().Str("Source", "stderr").Logger().WithContext(ctx), stderr)
   165  
   166  	// Look up the IP address of the gateway container and attach it to the spec
   167  	var containerDetails types.ContainerJSON
   168  	for {
   169  		containerDetails, err = e.client.ContainerInspect(ctx, gatewayContainer.ID)
   170  		if err != nil {
   171  			return nil, nil, errors.Wrap(err, "error getting gateway container details")
   172  		}
   173  		switch containerDetails.State.Health.Status {
   174  		case types.NoHealthcheck:
   175  			return nil, nil, errors.New("expecting gateway image to have healthcheck defined")
   176  		case types.Unhealthy:
   177  			return nil, nil, errors.New("gateway container failed to start")
   178  		case types.Starting:
   179  			time.Sleep(httpGatewayHealthcheckInterval)
   180  			continue
   181  		}
   182  
   183  		break
   184  	}
   185  
   186  	networkAttachment, ok := containerDetails.NetworkSettings.Networks[internalNetwork.Name]
   187  	if !ok || networkAttachment.IPAddress == "" {
   188  		return nil, nil, fmt.Errorf("gateway does not appear to be attached to internal network")
   189  	}
   190  	proxyIP := net.ParseIP(networkAttachment.IPAddress)
   191  	proxyAddr := net.TCPAddr{IP: proxyIP, Port: httpProxyPort}
   192  	return &internalNetwork, &proxyAddr, err
   193  }