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