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