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 }