github.com/ewagmig/fabric@v2.1.1+incompatible/core/chaincode/platforms/util/utils.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package util 8 9 import ( 10 "bytes" 11 "fmt" 12 "io" 13 "runtime" 14 "strings" 15 16 docker "github.com/fsouza/go-dockerclient" 17 "github.com/hyperledger/fabric/common/flogging" 18 "github.com/hyperledger/fabric/common/metadata" 19 "github.com/spf13/viper" 20 ) 21 22 var logger = flogging.MustGetLogger("chaincode.platform.util") 23 24 type DockerBuildOptions struct { 25 Image string 26 Cmd string 27 Env []string 28 InputStream io.Reader 29 OutputStream io.Writer 30 } 31 32 //------------------------------------------------------------------------------------------- 33 // DockerBuild 34 //------------------------------------------------------------------------------------------- 35 // This function allows a "pass-through" build of chaincode within a docker container as 36 // an alternative to using standard "docker build" + Dockerfile mechanisms. The plain docker 37 // build is somewhat limiting due to the resulting image that is a superset composition of 38 // the build-time and run-time environments. This superset can be problematic on several 39 // fronts, such as a bloated image size, and additional security exposure associated with 40 // applications that are not needed, etc. 41 // 42 // Therefore, this mechanism creates a pipeline consisting of an ephemeral docker 43 // container that accepts source code as input, runs some function (e.g. "go build"), and 44 // outputs the result. The intention is that this output will be consumed as the basis of 45 // a streamlined container by installing the output into a downstream docker-build based on 46 // an appropriate minimal image. 47 // 48 // The input parameters are fairly simple: 49 // - Image: (optional) The builder image to use or "chaincode.builder" 50 // - Cmd: The command to execute inside the container. 51 // - InputStream: A tarball of files that will be expanded into /chaincode/input. 52 // - OutputStream: A tarball of files that will be gathered from /chaincode/output 53 // after successful execution of Cmd. 54 //------------------------------------------------------------------------------------------- 55 func DockerBuild(opts DockerBuildOptions, client *docker.Client) error { 56 if opts.Image == "" { 57 opts.Image = GetDockerImageFromConfig("chaincode.builder") 58 if opts.Image == "" { 59 return fmt.Errorf("No image provided and \"chaincode.builder\" default does not exist") 60 } 61 } 62 63 logger.Debugf("Attempting build with image %s", opts.Image) 64 65 //----------------------------------------------------------------------------------- 66 // Ensure the image exists locally, or pull it from a registry if it doesn't 67 //----------------------------------------------------------------------------------- 68 _, err := client.InspectImage(opts.Image) 69 if err != nil { 70 logger.Debugf("Image %s does not exist locally, attempt pull", opts.Image) 71 72 err = client.PullImage(docker.PullImageOptions{Repository: opts.Image}, docker.AuthConfiguration{}) 73 if err != nil { 74 return fmt.Errorf("Failed to pull %s: %s", opts.Image, err) 75 } 76 } 77 78 //----------------------------------------------------------------------------------- 79 // Create an ephemeral container, armed with our Image/Cmd 80 //----------------------------------------------------------------------------------- 81 container, err := client.CreateContainer(docker.CreateContainerOptions{ 82 Config: &docker.Config{ 83 Image: opts.Image, 84 Cmd: []string{"/bin/sh", "-c", opts.Cmd}, 85 Env: opts.Env, 86 AttachStdout: true, 87 AttachStderr: true, 88 }, 89 }) 90 if err != nil { 91 return fmt.Errorf("Error creating container: %s", err) 92 } 93 defer client.RemoveContainer(docker.RemoveContainerOptions{ID: container.ID}) 94 95 //----------------------------------------------------------------------------------- 96 // Upload our input stream 97 //----------------------------------------------------------------------------------- 98 err = client.UploadToContainer(container.ID, docker.UploadToContainerOptions{ 99 Path: "/chaincode/input", 100 InputStream: opts.InputStream, 101 }) 102 if err != nil { 103 return fmt.Errorf("Error uploading input to container: %s", err) 104 } 105 106 //----------------------------------------------------------------------------------- 107 // Attach stdout buffer to capture possible compilation errors 108 //----------------------------------------------------------------------------------- 109 stdout := bytes.NewBuffer(nil) 110 cw, err := client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{ 111 Container: container.ID, 112 OutputStream: stdout, 113 ErrorStream: stdout, 114 Logs: true, 115 Stdout: true, 116 Stderr: true, 117 Stream: true, 118 }) 119 if err != nil { 120 return fmt.Errorf("Error attaching to container: %s", err) 121 } 122 123 //----------------------------------------------------------------------------------- 124 // Launch the actual build, realizing the Env/Cmd specified at container creation 125 //----------------------------------------------------------------------------------- 126 err = client.StartContainer(container.ID, nil) 127 if err != nil { 128 cw.Close() 129 return fmt.Errorf("Error executing build: %s \"%s\"", err, stdout.String()) 130 } 131 132 //----------------------------------------------------------------------------------- 133 // Wait for the build to complete and gather the return value 134 //----------------------------------------------------------------------------------- 135 retval, err := client.WaitContainer(container.ID) 136 if err != nil { 137 cw.Close() 138 return fmt.Errorf("Error waiting for container to complete: %s", err) 139 } 140 141 // Wait for stream copying to complete before accessing stdout. 142 cw.Close() 143 if err := cw.Wait(); err != nil { 144 logger.Errorf("attach wait failed: %s", err) 145 } 146 147 if retval > 0 { 148 return fmt.Errorf("Error returned from build: %d \"%s\"", retval, stdout.String()) 149 } 150 151 logger.Debugf("Build output is %s", stdout.String()) 152 153 //----------------------------------------------------------------------------------- 154 // Finally, download the result 155 //----------------------------------------------------------------------------------- 156 err = client.DownloadFromContainer(container.ID, docker.DownloadFromContainerOptions{ 157 Path: "/chaincode/output/.", 158 OutputStream: opts.OutputStream, 159 }) 160 if err != nil { 161 return fmt.Errorf("Error downloading output: %s", err) 162 } 163 164 return nil 165 } 166 167 // GetDockerImageFromConfig replaces variables in the config 168 func GetDockerImageFromConfig(path string) string { 169 r := strings.NewReplacer( 170 "$(ARCH)", runtime.GOARCH, 171 "$(PROJECT_VERSION)", metadata.Version, 172 "$(TWO_DIGIT_VERSION)", twoDigitVersion(metadata.Version), 173 "$(DOCKER_NS)", metadata.DockerNamespace) 174 175 return r.Replace(viper.GetString(path)) 176 } 177 178 // twoDigitVersion truncates a 3 digit version (e.g. 2.0.0) to a 2 digit version (e.g. 2.0), 179 // If version does not include dots (e.g. latest), just return the passed version 180 func twoDigitVersion(version string) string { 181 if strings.LastIndex(version, ".") < 0 { 182 return version 183 } 184 return version[0:strings.LastIndex(version, ".")] 185 }