github.com/myafeier/fabric@v1.0.1-0.20170722181825-3a4b1f2bce86/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, prelaunchFunc container.PrelaunchFunc) 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 if prelaunchFunc != nil { 334 if err = prelaunchFunc(); err != nil { 335 return err 336 } 337 } 338 339 // start container with HostConfig was deprecated since v1.10 and removed in v1.2 340 err = client.StartContainer(containerID, nil) 341 if err != nil { 342 dockerLogger.Errorf("start-could not start container: %s", err) 343 return err 344 } 345 346 dockerLogger.Debugf("Started container %s", containerID) 347 return nil 348 } 349 350 //Stop stops a running chaincode 351 func (vm *DockerVM) Stop(ctxt context.Context, ccid ccintf.CCID, timeout uint, dontkill bool, dontremove bool) error { 352 id, err := vm.GetVMName(ccid) 353 if err != nil { 354 return err 355 } 356 357 client, err := vm.getClientFnc() 358 if err != nil { 359 dockerLogger.Debugf("stop - cannot create client %s", err) 360 return err 361 } 362 id = strings.Replace(id, ":", "_", -1) 363 364 err = vm.stopInternal(ctxt, client, id, timeout, dontkill, dontremove) 365 366 return err 367 } 368 369 func (vm *DockerVM) stopInternal(ctxt context.Context, client dockerClient, 370 id string, timeout uint, dontkill bool, dontremove bool) error { 371 err := client.StopContainer(id, timeout) 372 if err != nil { 373 dockerLogger.Debugf("Stop container %s(%s)", id, err) 374 } else { 375 dockerLogger.Debugf("Stopped container %s", id) 376 } 377 if !dontkill { 378 err = client.KillContainer(docker.KillContainerOptions{ID: id}) 379 if err != nil { 380 dockerLogger.Debugf("Kill container %s (%s)", id, err) 381 } else { 382 dockerLogger.Debugf("Killed container %s", id) 383 } 384 } 385 if !dontremove { 386 err = client.RemoveContainer(docker.RemoveContainerOptions{ID: id, Force: true}) 387 if err != nil { 388 dockerLogger.Debugf("Remove container %s (%s)", id, err) 389 } else { 390 dockerLogger.Debugf("Removed container %s", id) 391 } 392 } 393 return err 394 } 395 396 //Destroy destroys an image 397 func (vm *DockerVM) Destroy(ctxt context.Context, ccid ccintf.CCID, force bool, noprune bool) error { 398 id, err := vm.GetVMName(ccid) 399 if err != nil { 400 return err 401 } 402 403 client, err := vm.getClientFnc() 404 if err != nil { 405 dockerLogger.Errorf("destroy-cannot create client %s", err) 406 return err 407 } 408 id = strings.Replace(id, ":", "_", -1) 409 410 err = client.RemoveImageExtended(id, docker.RemoveImageOptions{Force: force, NoPrune: noprune}) 411 412 if err != nil { 413 dockerLogger.Errorf("error while destroying image: %s", err) 414 } else { 415 dockerLogger.Debugf("Destroyed image %s", id) 416 } 417 418 return err 419 } 420 421 //GetVMName generates the docker image from peer information given the hashcode. This is needed to 422 //keep image name's unique in a single host, multi-peer environment (such as a development environment) 423 func (vm *DockerVM) GetVMName(ccid ccintf.CCID) (string, error) { 424 name := ccid.GetName() 425 426 //Convert to lowercase and replace any invalid characters with "-" 427 r := regexp.MustCompile("[^a-zA-Z0-9-_.]") 428 429 if ccid.NetworkID != "" && ccid.PeerID != "" { 430 name = strings.ToLower( 431 r.ReplaceAllString( 432 fmt.Sprintf("%s-%s-%s", ccid.NetworkID, ccid.PeerID, name), "-")) 433 } else if ccid.NetworkID != "" { 434 name = strings.ToLower( 435 r.ReplaceAllString( 436 fmt.Sprintf("%s-%s", ccid.NetworkID, name), "-")) 437 } else if ccid.PeerID != "" { 438 name = strings.ToLower( 439 r.ReplaceAllString( 440 fmt.Sprintf("%s-%s", ccid.PeerID, name), "-")) 441 } 442 443 // Check name complies with Docker's repository naming rules 444 r = regexp.MustCompile("^[a-z0-9]+(([._-][a-z0-9]+)+)?$") 445 446 if !r.MatchString(name) { 447 dockerLogger.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", name) 448 return name, fmt.Errorf("Error constructing Docker VM Name. '%s' breaks Docker's repository naming rules", name) 449 } 450 return name, nil 451 }