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  }