github.com/yous1230/fabric@v2.0.0-beta.0.20191224111736-74345bee6ac2+incompatible/core/container/dockercontroller/dockercontroller.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package dockercontroller 8 9 import ( 10 "archive/tar" 11 "bufio" 12 "bytes" 13 "compress/gzip" 14 "context" 15 "encoding/base64" 16 "encoding/hex" 17 "fmt" 18 "io" 19 "regexp" 20 "strconv" 21 "strings" 22 "time" 23 24 docker "github.com/fsouza/go-dockerclient" 25 pb "github.com/hyperledger/fabric-protos-go/peer" 26 "github.com/hyperledger/fabric/common/flogging" 27 "github.com/hyperledger/fabric/common/util" 28 "github.com/hyperledger/fabric/core/chaincode/persistence" 29 "github.com/hyperledger/fabric/core/container" 30 "github.com/hyperledger/fabric/core/container/ccintf" 31 "github.com/pkg/errors" 32 ) 33 34 var ( 35 dockerLogger = flogging.MustGetLogger("dockercontroller") 36 vmRegExp = regexp.MustCompile("[^a-zA-Z0-9-_.]") 37 imageRegExp = regexp.MustCompile("^[a-z0-9]+(([._-][a-z0-9]+)+)?$") 38 ) 39 40 //go:generate counterfeiter -o mock/dockerclient.go --fake-name DockerClient . dockerClient 41 42 // dockerClient represents a docker client 43 type dockerClient interface { 44 // CreateContainer creates a docker container, returns an error in case of failure 45 CreateContainer(opts docker.CreateContainerOptions) (*docker.Container, error) 46 // UploadToContainer uploads a tar archive to be extracted to a path in the 47 // filesystem of the container. 48 UploadToContainer(id string, opts docker.UploadToContainerOptions) error 49 // StartContainer starts a docker container, returns an error in case of failure 50 StartContainer(id string, cfg *docker.HostConfig) error 51 // AttachToContainer attaches to a docker container, returns an error in case of 52 // failure 53 AttachToContainer(opts docker.AttachToContainerOptions) error 54 // BuildImage builds an image from a tarball's url or a Dockerfile in the input 55 // stream, returns an error in case of failure 56 BuildImage(opts docker.BuildImageOptions) error 57 // StopContainer stops a docker container, killing it after the given timeout 58 // (in seconds). Returns an error in case of failure 59 StopContainer(id string, timeout uint) error 60 // KillContainer sends a signal to a docker container, returns an error in 61 // case of failure 62 KillContainer(opts docker.KillContainerOptions) error 63 // RemoveContainer removes a docker container, returns an error in case of failure 64 RemoveContainer(opts docker.RemoveContainerOptions) error 65 // PingWithContext pings the docker daemon. The context object can be used 66 // to cancel the ping request. 67 PingWithContext(context.Context) error 68 // WaitContainer blocks until the given container stops, and returns the exit 69 // code of the container status. 70 WaitContainer(containerID string) (int, error) 71 // InspectImage returns an image by its name or ID. 72 InspectImage(imageName string) (*docker.Image, error) 73 } 74 75 type PlatformBuilder interface { 76 GenerateDockerBuild(ccType, path string, codePackage io.Reader) (io.Reader, error) 77 } 78 79 type ContainerInstance struct { 80 CCID string 81 Type string 82 DockerVM *DockerVM 83 } 84 85 func (ci *ContainerInstance) Start(peerConnection *ccintf.PeerConnection) error { 86 return ci.DockerVM.Start(ci.CCID, ci.Type, peerConnection) 87 } 88 89 func (ci *ContainerInstance) ChaincodeServerInfo() (*ccintf.ChaincodeServerInfo, error) { 90 return nil, nil 91 } 92 93 func (ci *ContainerInstance) Stop() error { 94 return ci.DockerVM.Stop(ci.CCID) 95 } 96 97 func (ci *ContainerInstance) Wait() (int, error) { 98 return ci.DockerVM.Wait(ci.CCID) 99 } 100 101 // DockerVM is a vm. It is identified by an image id 102 type DockerVM struct { 103 PeerID string 104 NetworkID string 105 BuildMetrics *BuildMetrics 106 HostConfig *docker.HostConfig 107 Client dockerClient 108 AttachStdOut bool 109 ChaincodePull bool 110 NetworkMode string 111 PlatformBuilder PlatformBuilder 112 LoggingEnv []string 113 } 114 115 // HealthCheck checks if the DockerVM is able to communicate with the Docker 116 // daemon. 117 func (vm *DockerVM) HealthCheck(ctx context.Context) error { 118 if err := vm.Client.PingWithContext(ctx); err != nil { 119 return errors.Wrap(err, "failed to ping to Docker daemon") 120 } 121 return nil 122 } 123 124 func (vm *DockerVM) createContainer(imageID, containerID string, args, env []string) error { 125 logger := dockerLogger.With("imageID", imageID, "containerID", containerID) 126 logger.Debugw("create container") 127 _, err := vm.Client.CreateContainer(docker.CreateContainerOptions{ 128 Name: containerID, 129 Config: &docker.Config{ 130 Cmd: args, 131 Image: imageID, 132 Env: env, 133 AttachStdout: vm.AttachStdOut, 134 AttachStderr: vm.AttachStdOut, 135 }, 136 HostConfig: vm.HostConfig, 137 }) 138 if err != nil { 139 return err 140 } 141 logger.Debugw("created container") 142 return nil 143 } 144 145 func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { 146 id, err := vm.GetVMNameForDocker(ccid) 147 if err != nil { 148 return err 149 } 150 151 outputbuf := bytes.NewBuffer(nil) 152 opts := docker.BuildImageOptions{ 153 Name: id, 154 Pull: vm.ChaincodePull, 155 NetworkMode: vm.NetworkMode, 156 InputStream: reader, 157 OutputStream: outputbuf, 158 } 159 160 startTime := time.Now() 161 err = vm.Client.BuildImage(opts) 162 163 vm.BuildMetrics.ChaincodeImageBuildDuration.With( 164 "chaincode", ccid, 165 "success", strconv.FormatBool(err == nil), 166 ).Observe(time.Since(startTime).Seconds()) 167 168 if err != nil { 169 dockerLogger.Errorf("Error building image: %s", err) 170 dockerLogger.Errorf("Build Output:\n********************\n%s\n********************", outputbuf.String()) 171 return err 172 } 173 174 dockerLogger.Debugf("Created image: %s", id) 175 return nil 176 } 177 178 // Build is responsible for building an image if it does not already exist. 179 func (vm *DockerVM) Build(ccid string, metadata *persistence.ChaincodePackageMetadata, codePackage io.Reader) (container.Instance, error) { 180 imageName, err := vm.GetVMNameForDocker(ccid) 181 if err != nil { 182 return nil, err 183 } 184 185 // This is an awkward translation, but better here in a future dead path 186 // than elsewhere. The old enum types are capital, but at least as implemented 187 // lifecycle tools seem to allow type to be set lower case. 188 ccType := strings.ToUpper(metadata.Type) 189 190 _, err = vm.Client.InspectImage(imageName) 191 switch err { 192 case docker.ErrNoSuchImage: 193 dockerfileReader, err := vm.PlatformBuilder.GenerateDockerBuild(ccType, metadata.Path, codePackage) 194 if err != nil { 195 return nil, errors.Wrap(err, "platform builder failed") 196 } 197 err = vm.buildImage(ccid, dockerfileReader) 198 if err != nil { 199 return nil, errors.Wrap(err, "docker image build failed") 200 } 201 case nil: 202 default: 203 return nil, errors.Wrap(err, "docker image inspection failed") 204 } 205 206 return &ContainerInstance{ 207 DockerVM: vm, 208 CCID: ccid, 209 Type: ccType, 210 }, nil 211 } 212 213 // In order to support starting chaincode containers built with Fabric v1.4 and earlier, 214 // we must check for the precense of the start.sh script for Node.js chaincode before 215 // attempting to call it. 216 var nodeStartScript = ` 217 set -e 218 if [ -x /chaincode/start.sh ]; then 219 /chaincode/start.sh --peer.address %[1]s 220 else 221 cd /usr/local/src 222 npm start -- --peer.address %[1]s 223 fi 224 ` 225 226 func (vm *DockerVM) GetArgs(ccType string, peerAddress string) ([]string, error) { 227 // language specific arguments, possibly should be pushed back into platforms, but were simply 228 // ported from the container_runtime chaincode component 229 switch ccType { 230 case pb.ChaincodeSpec_GOLANG.String(), pb.ChaincodeSpec_CAR.String(): 231 return []string{"chaincode", fmt.Sprintf("-peer.address=%s", peerAddress)}, nil 232 case pb.ChaincodeSpec_JAVA.String(): 233 return []string{"/root/chaincode-java/start", "--peerAddress", peerAddress}, nil 234 case pb.ChaincodeSpec_NODE.String(): 235 return []string{"/bin/sh", "-c", fmt.Sprintf(nodeStartScript, peerAddress)}, nil 236 default: 237 return nil, errors.Errorf("unknown chaincodeType: %s", ccType) 238 } 239 } 240 241 const ( 242 // Mutual TLS auth client key and cert paths in the chaincode container 243 TLSClientKeyPath string = "/etc/hyperledger/fabric/client.key" 244 TLSClientCertPath string = "/etc/hyperledger/fabric/client.crt" 245 TLSClientKeyFile string = "/etc/hyperledger/fabric/client_pem.key" 246 TLSClientCertFile string = "/etc/hyperledger/fabric/client_pem.crt" 247 TLSClientRootCertFile string = "/etc/hyperledger/fabric/peer.crt" 248 ) 249 250 func (vm *DockerVM) GetEnv(ccid string, tlsConfig *ccintf.TLSConfig) []string { 251 // common environment variables 252 // FIXME: we are using the env variable CHAINCODE_ID to store 253 // the package ID; in the legacy lifecycle they used to be the 254 // same but now they are not, so we should use a different env 255 // variable. However chaincodes built by older versions of the 256 // peer still adopt this broken convention. (FAB-14630) 257 envs := []string{fmt.Sprintf("CORE_CHAINCODE_ID_NAME=%s", ccid)} 258 envs = append(envs, vm.LoggingEnv...) 259 260 // Pass TLS options to chaincode 261 if tlsConfig != nil { 262 envs = append(envs, "CORE_PEER_TLS_ENABLED=true") 263 envs = append(envs, fmt.Sprintf("CORE_TLS_CLIENT_KEY_PATH=%s", TLSClientKeyPath)) 264 envs = append(envs, fmt.Sprintf("CORE_TLS_CLIENT_CERT_PATH=%s", TLSClientCertPath)) 265 envs = append(envs, fmt.Sprintf("CORE_TLS_CLIENT_KEY_FILE=%s", TLSClientKeyFile)) 266 envs = append(envs, fmt.Sprintf("CORE_TLS_CLIENT_CERT_FILE=%s", TLSClientCertFile)) 267 envs = append(envs, fmt.Sprintf("CORE_PEER_TLS_ROOTCERT_FILE=%s", TLSClientRootCertFile)) 268 } else { 269 envs = append(envs, "CORE_PEER_TLS_ENABLED=false") 270 } 271 272 return envs 273 } 274 275 // Start starts a container using a previously created docker image 276 func (vm *DockerVM) Start(ccid string, ccType string, peerConnection *ccintf.PeerConnection) error { 277 imageName, err := vm.GetVMNameForDocker(ccid) 278 if err != nil { 279 return err 280 } 281 282 containerName := vm.GetVMName(ccid) 283 logger := dockerLogger.With("imageName", imageName, "containerName", containerName) 284 285 vm.stopInternal(containerName) 286 287 args, err := vm.GetArgs(ccType, peerConnection.Address) 288 if err != nil { 289 return errors.WithMessage(err, "could not get args") 290 } 291 dockerLogger.Debugf("start container with args: %s", strings.Join(args, " ")) 292 293 env := vm.GetEnv(ccid, peerConnection.TLSConfig) 294 dockerLogger.Debugf("start container with env:\n\t%s", strings.Join(env, "\n\t")) 295 296 err = vm.createContainer(imageName, containerName, args, env) 297 if err != nil { 298 logger.Errorf("create container failed: %s", err) 299 return err 300 } 301 302 // stream stdout and stderr to chaincode logger 303 if vm.AttachStdOut { 304 containerLogger := flogging.MustGetLogger("peer.chaincode." + containerName) 305 streamOutput(dockerLogger, vm.Client, containerName, containerLogger) 306 } 307 308 // upload TLS files to the container before starting it if needed 309 if peerConnection.TLSConfig != nil { 310 // the docker upload API takes a tar file, so we need to first 311 // consolidate the file entries to a tar 312 payload := bytes.NewBuffer(nil) 313 gw := gzip.NewWriter(payload) 314 tw := tar.NewWriter(gw) 315 316 // Note, we goofily base64 encode 2 of the TLS artifacts but not the other for strange historical reasons 317 err = addFiles(tw, map[string][]byte{ 318 TLSClientKeyPath: []byte(base64.StdEncoding.EncodeToString(peerConnection.TLSConfig.ClientKey)), 319 TLSClientCertPath: []byte(base64.StdEncoding.EncodeToString(peerConnection.TLSConfig.ClientCert)), 320 TLSClientKeyFile: peerConnection.TLSConfig.ClientKey, 321 TLSClientCertFile: peerConnection.TLSConfig.ClientCert, 322 TLSClientRootCertFile: peerConnection.TLSConfig.RootCert, 323 }) 324 if err != nil { 325 return fmt.Errorf("error writing files to upload to Docker instance into a temporary tar blob: %s", err) 326 } 327 328 // Write the tar file out 329 if err := tw.Close(); err != nil { 330 return fmt.Errorf("error writing files to upload to Docker instance into a temporary tar blob: %s", err) 331 } 332 333 gw.Close() 334 335 err := vm.Client.UploadToContainer(containerName, docker.UploadToContainerOptions{ 336 InputStream: bytes.NewReader(payload.Bytes()), 337 Path: "/", 338 NoOverwriteDirNonDir: false, 339 }) 340 if err != nil { 341 return fmt.Errorf("Error uploading files to the container instance %s: %s", containerName, err) 342 } 343 } 344 345 // start container with HostConfig was deprecated since v1.10 and removed in v1.2 346 err = vm.Client.StartContainer(containerName, nil) 347 if err != nil { 348 dockerLogger.Errorf("start-could not start container: %s", err) 349 return err 350 } 351 352 dockerLogger.Debugf("Started container %s", containerName) 353 return nil 354 } 355 356 func addFiles(tw *tar.Writer, contents map[string][]byte) error { 357 for name, payload := range contents { 358 err := tw.WriteHeader(&tar.Header{ 359 Name: name, 360 Size: int64(len(payload)), 361 Mode: 0100644, 362 }) 363 if err != nil { 364 return err 365 } 366 367 _, err = tw.Write(payload) 368 if err != nil { 369 return err 370 } 371 } 372 373 return nil 374 } 375 376 // streamOutput mirrors output from the named container to a fabric logger. 377 func streamOutput(logger *flogging.FabricLogger, client dockerClient, containerName string, containerLogger *flogging.FabricLogger) { 378 // Launch a few go routines to manage output streams from the container. 379 // They will be automatically destroyed when the container exits 380 attached := make(chan struct{}) 381 r, w := io.Pipe() 382 383 go func() { 384 // AttachToContainer will fire off a message on the "attached" channel once the 385 // attachment completes, and then block until the container is terminated. 386 // The returned error is not used outside the scope of this function. Assign the 387 // error to a local variable to prevent clobbering the function variable 'err'. 388 err := client.AttachToContainer(docker.AttachToContainerOptions{ 389 Container: containerName, 390 OutputStream: w, 391 ErrorStream: w, 392 Logs: true, 393 Stdout: true, 394 Stderr: true, 395 Stream: true, 396 Success: attached, 397 }) 398 399 // If we get here, the container has terminated. Send a signal on the pipe 400 // so that downstream may clean up appropriately 401 _ = w.CloseWithError(err) 402 }() 403 404 go func() { 405 defer r.Close() // ensure the pipe reader gets closed 406 407 // Block here until the attachment completes or we timeout 408 select { 409 case <-attached: // successful attach 410 close(attached) // close indicates the streams can now be copied 411 412 case <-time.After(10 * time.Second): 413 logger.Errorf("Timeout while attaching to IO channel in container %s", containerName) 414 return 415 } 416 417 is := bufio.NewReader(r) 418 for { 419 // Loop forever dumping lines of text into the containerLogger 420 // until the pipe is closed 421 line, err := is.ReadString('\n') 422 switch err { 423 case nil: 424 containerLogger.Info(line) 425 case io.EOF: 426 logger.Infof("Container %s has closed its IO channel", containerName) 427 return 428 default: 429 logger.Errorf("Error reading container output: %s", err) 430 return 431 } 432 } 433 }() 434 } 435 436 // Stop stops a running chaincode 437 func (vm *DockerVM) Stop(ccid string) error { 438 id := vm.ccidToContainerID(ccid) 439 return vm.stopInternal(id) 440 } 441 442 // Wait blocks until the container stops and returns the exit code of the container. 443 func (vm *DockerVM) Wait(ccid string) (int, error) { 444 id := vm.ccidToContainerID(ccid) 445 return vm.Client.WaitContainer(id) 446 } 447 448 func (vm *DockerVM) ccidToContainerID(ccid string) string { 449 return strings.Replace(vm.GetVMName(ccid), ":", "_", -1) 450 } 451 452 func (vm *DockerVM) stopInternal(id string) error { 453 logger := dockerLogger.With("id", id) 454 455 logger.Debugw("stopping container") 456 err := vm.Client.StopContainer(id, 0) 457 dockerLogger.Debugw("stop container result", "error", err) 458 459 logger.Debugw("killing container") 460 err = vm.Client.KillContainer(docker.KillContainerOptions{ID: id}) 461 logger.Debugw("kill container result", "error", err) 462 463 logger.Debugw("removing container") 464 err = vm.Client.RemoveContainer(docker.RemoveContainerOptions{ID: id, Force: true}) 465 logger.Debugw("remove container result", "error", err) 466 467 return err 468 } 469 470 // GetVMName generates the VM name from peer information. It accepts a format 471 // function parameter to allow different formatting based on the desired use of 472 // the name. 473 func (vm *DockerVM) GetVMName(ccid string) string { 474 // replace any invalid characters with "-" (either in network id, peer id, or in the 475 // entire name returned by any format function) 476 return vmRegExp.ReplaceAllString(vm.preFormatImageName(ccid), "-") 477 } 478 479 // GetVMNameForDocker formats the docker image from peer information. This is 480 // needed to keep image (repository) names unique in a single host, multi-peer 481 // environment (such as a development environment). It computes the hash for the 482 // supplied image name and then appends it to the lowercase image name to ensure 483 // uniqueness. 484 func (vm *DockerVM) GetVMNameForDocker(ccid string) (string, error) { 485 name := vm.preFormatImageName(ccid) 486 // pre-2.0 used "-" as the separator in the ccid, so replace ":" with 487 // "-" here to ensure 2.0 peers can find pre-2.0 cc images 488 name = strings.ReplaceAll(name, ":", "-") 489 hash := hex.EncodeToString(util.ComputeSHA256([]byte(name))) 490 saniName := vmRegExp.ReplaceAllString(name, "-") 491 imageName := strings.ToLower(fmt.Sprintf("%s-%s", saniName, hash)) 492 493 // Check that name complies with Docker's repository naming rules 494 if !imageRegExp.MatchString(imageName) { 495 dockerLogger.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", name) 496 return "", fmt.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", imageName) 497 } 498 499 return imageName, nil 500 } 501 502 func (vm *DockerVM) preFormatImageName(ccid string) string { 503 name := ccid 504 505 if vm.NetworkID != "" && vm.PeerID != "" { 506 name = fmt.Sprintf("%s-%s-%s", vm.NetworkID, vm.PeerID, name) 507 } else if vm.NetworkID != "" { 508 name = fmt.Sprintf("%s-%s", vm.NetworkID, name) 509 } else if vm.PeerID != "" { 510 name = fmt.Sprintf("%s-%s", vm.PeerID, name) 511 } 512 513 return name 514 }