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  }