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 }