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