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  }