github.com/tenywen/fabric@v1.0.0-beta.0.20170620030522-a5b1ed380643/core/container/dockercontroller/dockercontroller.go (about)

     1  /*
     2  Copyright IBM Corp. 2016 All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  		 http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package dockercontroller
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  	"time"
    25  
    26  	"bufio"
    27  
    28  	"regexp"
    29  
    30  	"github.com/fsouza/go-dockerclient"
    31  	"github.com/hyperledger/fabric/common/flogging"
    32  	container "github.com/hyperledger/fabric/core/container/api"
    33  	"github.com/hyperledger/fabric/core/container/ccintf"
    34  	cutil "github.com/hyperledger/fabric/core/container/util"
    35  	"github.com/op/go-logging"
    36  	"github.com/spf13/viper"
    37  	"golang.org/x/net/context"
    38  )
    39  
    40  var (
    41  	dockerLogger = flogging.MustGetLogger("dockercontroller")
    42  	hostConfig   *docker.HostConfig
    43  )
    44  
    45  // getClient returns an instance that implements dockerClient interface
    46  type getClient func() (dockerClient, error)
    47  
    48  //DockerVM is a vm. It is identified by an image id
    49  type DockerVM struct {
    50  	id           string
    51  	getClientFnc getClient
    52  }
    53  
    54  // dockerClient represents a docker client
    55  type dockerClient interface {
    56  	// CreateContainer creates a docker container, returns an error in case of failure
    57  	CreateContainer(opts docker.CreateContainerOptions) (*docker.Container, error)
    58  	// StartContainer starts a docker container, returns an error in case of failure
    59  	StartContainer(id string, cfg *docker.HostConfig) error
    60  	// AttachToContainer attaches to a docker container, returns an error in case of
    61  	// failure
    62  	AttachToContainer(opts docker.AttachToContainerOptions) error
    63  	// BuildImage builds an image from a tarball's url or a Dockerfile in the input
    64  	// stream, returns an error in case of failure
    65  	BuildImage(opts docker.BuildImageOptions) error
    66  	// RemoveImageExtended removes a docker image by its name or ID, returns an
    67  	// error in case of failure
    68  	RemoveImageExtended(id string, opts docker.RemoveImageOptions) error
    69  	// StopContainer stops a docker container, killing it after the given timeout
    70  	// (in seconds). Returns an error in case of failure
    71  	StopContainer(id string, timeout uint) error
    72  	// KillContainer sends a signal to a docker container, returns an error in
    73  	// case of failure
    74  	KillContainer(opts docker.KillContainerOptions) error
    75  	// RemoveContainer removes a docker container, returns an error in case of failure
    76  	RemoveContainer(opts docker.RemoveContainerOptions) error
    77  }
    78  
    79  // NewDockerVM returns a new DockerVM instance
    80  func NewDockerVM() *DockerVM {
    81  	vm := DockerVM{}
    82  	vm.getClientFnc = getDockerClient
    83  	return &vm
    84  }
    85  
    86  func getDockerClient() (dockerClient, error) {
    87  	return cutil.NewDockerClient()
    88  }
    89  
    90  func getDockerHostConfig() *docker.HostConfig {
    91  	if hostConfig != nil {
    92  		return hostConfig
    93  	}
    94  	dockerKey := func(key string) string {
    95  		return "vm.docker.hostConfig." + key
    96  	}
    97  	getInt64 := func(key string) int64 {
    98  		defer func() {
    99  			if err := recover(); err != nil {
   100  				dockerLogger.Warningf("load vm.docker.hostConfig.%s failed, error: %v", key, err)
   101  			}
   102  		}()
   103  		n := viper.GetInt(dockerKey(key))
   104  		return int64(n)
   105  	}
   106  
   107  	var logConfig docker.LogConfig
   108  	err := viper.UnmarshalKey(dockerKey("LogConfig"), &logConfig)
   109  	if err != nil {
   110  		dockerLogger.Warningf("load docker HostConfig.LogConfig failed, error: %s", err.Error())
   111  	}
   112  	networkMode := viper.GetString(dockerKey("NetworkMode"))
   113  	if networkMode == "" {
   114  		networkMode = "host"
   115  	}
   116  	dockerLogger.Debugf("docker container hostconfig NetworkMode: %s", networkMode)
   117  
   118  	hostConfig = &docker.HostConfig{
   119  		CapAdd:  viper.GetStringSlice(dockerKey("CapAdd")),
   120  		CapDrop: viper.GetStringSlice(dockerKey("CapDrop")),
   121  
   122  		DNS:         viper.GetStringSlice(dockerKey("Dns")),
   123  		DNSSearch:   viper.GetStringSlice(dockerKey("DnsSearch")),
   124  		ExtraHosts:  viper.GetStringSlice(dockerKey("ExtraHosts")),
   125  		NetworkMode: networkMode,
   126  		IpcMode:     viper.GetString(dockerKey("IpcMode")),
   127  		PidMode:     viper.GetString(dockerKey("PidMode")),
   128  		UTSMode:     viper.GetString(dockerKey("UTSMode")),
   129  		LogConfig:   logConfig,
   130  
   131  		ReadonlyRootfs:   viper.GetBool(dockerKey("ReadonlyRootfs")),
   132  		SecurityOpt:      viper.GetStringSlice(dockerKey("SecurityOpt")),
   133  		CgroupParent:     viper.GetString(dockerKey("CgroupParent")),
   134  		Memory:           getInt64("Memory"),
   135  		MemorySwap:       getInt64("MemorySwap"),
   136  		MemorySwappiness: getInt64("MemorySwappiness"),
   137  		OOMKillDisable:   viper.GetBool(dockerKey("OomKillDisable")),
   138  		CPUShares:        getInt64("CpuShares"),
   139  		CPUSet:           viper.GetString(dockerKey("Cpuset")),
   140  		CPUSetCPUs:       viper.GetString(dockerKey("CpusetCPUs")),
   141  		CPUSetMEMs:       viper.GetString(dockerKey("CpusetMEMs")),
   142  		CPUQuota:         getInt64("CpuQuota"),
   143  		CPUPeriod:        getInt64("CpuPeriod"),
   144  		BlkioWeight:      getInt64("BlkioWeight"),
   145  	}
   146  
   147  	return hostConfig
   148  }
   149  
   150  func (vm *DockerVM) createContainer(ctxt context.Context, client dockerClient,
   151  	imageID string, containerID string, args []string,
   152  	env []string, attachStdout bool) error {
   153  	config := docker.Config{Cmd: args, Image: imageID, Env: env, AttachStdout: attachStdout, AttachStderr: attachStdout}
   154  	copts := docker.CreateContainerOptions{Name: containerID, Config: &config, HostConfig: getDockerHostConfig()}
   155  	dockerLogger.Debugf("Create container: %s", containerID)
   156  	_, err := client.CreateContainer(copts)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	dockerLogger.Debugf("Created container: %s", imageID)
   161  	return nil
   162  }
   163  
   164  func (vm *DockerVM) deployImage(client dockerClient, ccid ccintf.CCID,
   165  	args []string, env []string, reader io.Reader) error {
   166  	id, err := vm.GetVMName(ccid)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	outputbuf := bytes.NewBuffer(nil)
   171  	opts := docker.BuildImageOptions{
   172  		Name:         id,
   173  		Pull:         false,
   174  		InputStream:  reader,
   175  		OutputStream: outputbuf,
   176  	}
   177  
   178  	if err := client.BuildImage(opts); err != nil {
   179  		dockerLogger.Errorf("Error building images: %s", err)
   180  		dockerLogger.Errorf("Image Output:\n********************\n%s\n********************", outputbuf.String())
   181  		return err
   182  	}
   183  
   184  	dockerLogger.Debugf("Created image: %s", id)
   185  
   186  	return nil
   187  }
   188  
   189  //Deploy use the reader containing targz to create a docker image
   190  //for docker inputbuf is tar reader ready for use by docker.Client
   191  //the stream from end client to peer could directly be this tar stream
   192  //talk to docker daemon using docker Client and build the image
   193  func (vm *DockerVM) Deploy(ctxt context.Context, ccid ccintf.CCID,
   194  	args []string, env []string, reader io.Reader) error {
   195  
   196  	client, err := vm.getClientFnc()
   197  	switch err {
   198  	case nil:
   199  		if err = vm.deployImage(client, ccid, args, env, reader); err != nil {
   200  			return err
   201  		}
   202  	default:
   203  		return fmt.Errorf("Error creating docker client: %s", err)
   204  	}
   205  	return nil
   206  }
   207  
   208  //Start starts a container using a previously created docker image
   209  func (vm *DockerVM) Start(ctxt context.Context, ccid ccintf.CCID,
   210  	args []string, env []string, builder container.BuildSpecFactory) error {
   211  	imageID, err := vm.GetVMName(ccid)
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	client, err := vm.getClientFnc()
   217  	if err != nil {
   218  		dockerLogger.Debugf("start - cannot create client %s", err)
   219  		return err
   220  	}
   221  
   222  	containerID := strings.Replace(imageID, ":", "_", -1)
   223  	attachStdout := viper.GetBool("vm.docker.attachStdout")
   224  
   225  	//stop,force remove if necessary
   226  	dockerLogger.Debugf("Cleanup container %s", containerID)
   227  	vm.stopInternal(ctxt, client, containerID, 0, false, false)
   228  
   229  	dockerLogger.Debugf("Start container %s", containerID)
   230  	err = vm.createContainer(ctxt, client, imageID, containerID, args, env, attachStdout)
   231  	if err != nil {
   232  		//if image not found try to create image and retry
   233  		if err == docker.ErrNoSuchImage {
   234  			if builder != nil {
   235  				dockerLogger.Debugf("start-could not find image <%s> (container id <%s>), because of <%s>..."+
   236  					"attempt to recreate image", imageID, containerID, err)
   237  
   238  				reader, err1 := builder()
   239  				if err1 != nil {
   240  					dockerLogger.Errorf("Error creating image builder for image <%s> (container id <%s>), "+
   241  						"because of <%s>", imageID, containerID, err1)
   242  				}
   243  
   244  				if err1 = vm.deployImage(client, ccid, args, env, reader); err1 != nil {
   245  					return err1
   246  				}
   247  
   248  				dockerLogger.Debug("start-recreated image successfully")
   249  				if err1 = vm.createContainer(ctxt, client, imageID, containerID, args, env, attachStdout); err1 != nil {
   250  					dockerLogger.Errorf("start-could not recreate container post recreate image: %s", err1)
   251  					return err1
   252  				}
   253  			} else {
   254  				dockerLogger.Errorf("start-could not find image <%s>, because of %s", imageID, err)
   255  				return err
   256  			}
   257  		} else {
   258  			dockerLogger.Errorf("start-could not recreate container <%s>, because of %s", containerID, err)
   259  			return err
   260  		}
   261  	}
   262  
   263  	if attachStdout {
   264  		// Launch a few go-threads to manage output streams from the container.
   265  		// They will be automatically destroyed when the container exits
   266  		attached := make(chan struct{})
   267  		r, w := io.Pipe()
   268  
   269  		go func() {
   270  			// AttachToContainer will fire off a message on the "attached" channel once the
   271  			// attachment completes, and then block until the container is terminated.
   272  			// The returned error is not used outside the scope of this function. Assign the
   273  			// error to a local variable to prevent clobbering the function variable 'err'.
   274  			err := client.AttachToContainer(docker.AttachToContainerOptions{
   275  				Container:    containerID,
   276  				OutputStream: w,
   277  				ErrorStream:  w,
   278  				Logs:         true,
   279  				Stdout:       true,
   280  				Stderr:       true,
   281  				Stream:       true,
   282  				Success:      attached,
   283  			})
   284  
   285  			// If we get here, the container has terminated.  Send a signal on the pipe
   286  			// so that downstream may clean up appropriately
   287  			_ = w.CloseWithError(err)
   288  		}()
   289  
   290  		go func() {
   291  			// Block here until the attachment completes or we timeout
   292  			select {
   293  			case <-attached:
   294  				// successful attach
   295  			case <-time.After(10 * time.Second):
   296  				dockerLogger.Errorf("Timeout while attaching to IO channel in container %s", containerID)
   297  				return
   298  			}
   299  
   300  			// Acknowledge the attachment?  This was included in the gist I followed
   301  			// (http://bit.ly/2jBrCtM).  Not sure it's actually needed but it doesn't
   302  			// appear to hurt anything.
   303  			attached <- struct{}{}
   304  
   305  			// Establish a buffer for our IO channel so that we may do readline-style
   306  			// ingestion of the IO, one log entry per line
   307  			is := bufio.NewReader(r)
   308  
   309  			// Acquire a custom logger for our chaincode, inheriting the level from the peer
   310  			containerLogger := flogging.MustGetLogger(containerID)
   311  			logging.SetLevel(logging.GetLevel("peer"), containerID)
   312  
   313  			for {
   314  				// Loop forever dumping lines of text into the containerLogger
   315  				// until the pipe is closed
   316  				line, err2 := is.ReadString('\n')
   317  				if err2 != nil {
   318  					switch err2 {
   319  					case io.EOF:
   320  						dockerLogger.Infof("Container %s has closed its IO channel", containerID)
   321  					default:
   322  						dockerLogger.Errorf("Error reading container output: %s", err2)
   323  					}
   324  
   325  					return
   326  				}
   327  
   328  				containerLogger.Info(line)
   329  			}
   330  		}()
   331  	}
   332  
   333  	// start container with HostConfig was deprecated since v1.10 and removed in v1.2
   334  	err = client.StartContainer(containerID, nil)
   335  	if err != nil {
   336  		dockerLogger.Errorf("start-could not start container: %s", err)
   337  		return err
   338  	}
   339  
   340  	dockerLogger.Debugf("Started container %s", containerID)
   341  	return nil
   342  }
   343  
   344  //Stop stops a running chaincode
   345  func (vm *DockerVM) Stop(ctxt context.Context, ccid ccintf.CCID, timeout uint, dontkill bool, dontremove bool) error {
   346  	id, err := vm.GetVMName(ccid)
   347  	if err != nil {
   348  		return err
   349  	}
   350  
   351  	client, err := vm.getClientFnc()
   352  	if err != nil {
   353  		dockerLogger.Debugf("stop - cannot create client %s", err)
   354  		return err
   355  	}
   356  	id = strings.Replace(id, ":", "_", -1)
   357  
   358  	err = vm.stopInternal(ctxt, client, id, timeout, dontkill, dontremove)
   359  
   360  	return err
   361  }
   362  
   363  func (vm *DockerVM) stopInternal(ctxt context.Context, client dockerClient,
   364  	id string, timeout uint, dontkill bool, dontremove bool) error {
   365  	err := client.StopContainer(id, timeout)
   366  	if err != nil {
   367  		dockerLogger.Debugf("Stop container %s(%s)", id, err)
   368  	} else {
   369  		dockerLogger.Debugf("Stopped container %s", id)
   370  	}
   371  	if !dontkill {
   372  		err = client.KillContainer(docker.KillContainerOptions{ID: id})
   373  		if err != nil {
   374  			dockerLogger.Debugf("Kill container %s (%s)", id, err)
   375  		} else {
   376  			dockerLogger.Debugf("Killed container %s", id)
   377  		}
   378  	}
   379  	if !dontremove {
   380  		err = client.RemoveContainer(docker.RemoveContainerOptions{ID: id, Force: true})
   381  		if err != nil {
   382  			dockerLogger.Debugf("Remove container %s (%s)", id, err)
   383  		} else {
   384  			dockerLogger.Debugf("Removed container %s", id)
   385  		}
   386  	}
   387  	return err
   388  }
   389  
   390  //Destroy destroys an image
   391  func (vm *DockerVM) Destroy(ctxt context.Context, ccid ccintf.CCID, force bool, noprune bool) error {
   392  	id, err := vm.GetVMName(ccid)
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	client, err := vm.getClientFnc()
   398  	if err != nil {
   399  		dockerLogger.Errorf("destroy-cannot create client %s", err)
   400  		return err
   401  	}
   402  	id = strings.Replace(id, ":", "_", -1)
   403  
   404  	err = client.RemoveImageExtended(id, docker.RemoveImageOptions{Force: force, NoPrune: noprune})
   405  
   406  	if err != nil {
   407  		dockerLogger.Errorf("error while destroying image: %s", err)
   408  	} else {
   409  		dockerLogger.Debugf("Destroyed image %s", id)
   410  	}
   411  
   412  	return err
   413  }
   414  
   415  //GetVMName generates the docker image from peer information given the hashcode. This is needed to
   416  //keep image name's unique in a single host, multi-peer environment (such as a development environment)
   417  func (vm *DockerVM) GetVMName(ccid ccintf.CCID) (string, error) {
   418  	name := ccid.GetName()
   419  
   420  	//Convert to lowercase and replace any invalid characters with "-"
   421  	r := regexp.MustCompile("[^a-zA-Z0-9-_.]")
   422  
   423  	if ccid.NetworkID != "" && ccid.PeerID != "" {
   424  		name = strings.ToLower(
   425  			r.ReplaceAllString(
   426  				fmt.Sprintf("%s-%s-%s", ccid.NetworkID, ccid.PeerID, name), "-"))
   427  	} else if ccid.NetworkID != "" {
   428  		name = strings.ToLower(
   429  			r.ReplaceAllString(
   430  				fmt.Sprintf("%s-%s", ccid.NetworkID, name), "-"))
   431  	} else if ccid.PeerID != "" {
   432  		name = strings.ToLower(
   433  			r.ReplaceAllString(
   434  				fmt.Sprintf("%s-%s", ccid.PeerID, name), "-"))
   435  	}
   436  
   437  	// Check name complies with Docker's repository naming rules
   438  	r = regexp.MustCompile("^[a-z0-9]+(([._-][a-z0-9]+)+)?$")
   439  
   440  	if !r.MatchString(name) {
   441  		dockerLogger.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", name)
   442  		return name, fmt.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", name)
   443  	}
   444  	return name, nil
   445  }