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 }