github.com/yous1230/fabric@v2.0.0-beta.0.20191224111736-74345bee6ac2+incompatible/core/container/externalbuilder/externalbuilder.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package externalbuilder
     8  
     9  import (
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"regexp"
    18  	"time"
    19  
    20  	"github.com/hyperledger/fabric/common/flogging"
    21  	"github.com/hyperledger/fabric/core/container/ccintf"
    22  	"github.com/hyperledger/fabric/core/peer"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  var (
    27  	// DefaultEnvWhitelist enumerates the list of environment variables that are
    28  	// implicitly propagated to external builder and launcher commands.
    29  	DefaultEnvWhitelist = []string{"LD_LIBRARY_PATH", "LIBPATH", "PATH", "TMPDIR"}
    30  
    31  	logger = flogging.MustGetLogger("chaincode.externalbuilder")
    32  )
    33  
    34  // BuildInfo contains metadata is that is saved to the local file system with the
    35  // assets generated by an external builder. This is used to associate build output
    36  // with the builder that generated it.
    37  type BuildInfo struct {
    38  	// BuilderName is the user provided name of the external builder.
    39  	BuilderName string `json:"builder_name"`
    40  }
    41  
    42  // A Detector is responsible for orchestrating the external builder detection and
    43  // build process.
    44  type Detector struct {
    45  	// DurablePath is the file system location where chaincode assets are persisted.
    46  	DurablePath string
    47  	// Builders are the builders that detect and build processing will use.
    48  	Builders []*Builder
    49  }
    50  
    51  // CachedBuild returns a build instance that was already built or nil when no
    52  // instance has been found.  An error is returned only when an unexpected
    53  // condition is encountered.
    54  func (d *Detector) CachedBuild(ccid string) (*Instance, error) {
    55  	durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid))
    56  	_, err := os.Stat(durablePath)
    57  	if os.IsNotExist(err) {
    58  		return nil, nil
    59  	}
    60  	if err != nil {
    61  		return nil, errors.WithMessage(err, "existing build detected, but something went wrong inspecting it")
    62  	}
    63  
    64  	buildInfoPath := filepath.Join(durablePath, "build-info.json")
    65  	buildInfoData, err := ioutil.ReadFile(buildInfoPath)
    66  	if err != nil {
    67  		return nil, errors.WithMessagef(err, "could not read '%s' for build info", buildInfoPath)
    68  	}
    69  
    70  	var buildInfo BuildInfo
    71  	if err := json.Unmarshal(buildInfoData, &buildInfo); err != nil {
    72  		return nil, errors.WithMessagef(err, "malformed build info at '%s'", buildInfoPath)
    73  	}
    74  
    75  	for _, builder := range d.Builders {
    76  		if builder.Name == buildInfo.BuilderName {
    77  			return &Instance{
    78  				PackageID:   ccid,
    79  				Builder:     builder,
    80  				BldDir:      filepath.Join(durablePath, "bld"),
    81  				ReleaseDir:  filepath.Join(durablePath, "release"),
    82  				TermTimeout: 5 * time.Second,
    83  			}, nil
    84  		}
    85  	}
    86  
    87  	return nil, errors.Errorf("chaincode '%s' was already built with builder '%s', but that builder is no longer available", ccid, buildInfo.BuilderName)
    88  }
    89  
    90  // Build executes the external builder detect and build process.
    91  //
    92  // Before running the detect and build process, the detector first checks the
    93  // durable path for the results of a previous build for the provided package.
    94  // If found, the detect and build process is skipped and the existing instance
    95  // is returned.
    96  func (d *Detector) Build(ccid string, mdBytes []byte, codeStream io.Reader) (*Instance, error) {
    97  	// A small optimization: prevent exploding the build package out into the
    98  	// file system unless there are external builders defined.
    99  	if len(d.Builders) == 0 {
   100  		return nil, nil
   101  	}
   102  
   103  	// Look for a cached instance.
   104  	i, err := d.CachedBuild(ccid)
   105  	if err != nil {
   106  		return nil, errors.WithMessage(err, "existing build could not be restored")
   107  	}
   108  	if i != nil {
   109  		return i, nil
   110  	}
   111  
   112  	buildContext, err := NewBuildContext(ccid, mdBytes, codeStream)
   113  	if err != nil {
   114  		return nil, errors.WithMessage(err, "could not create build context")
   115  	}
   116  	defer buildContext.Cleanup()
   117  
   118  	builder := d.detect(buildContext)
   119  	if builder == nil {
   120  		logger.Debugf("no external builder detected for %s", ccid)
   121  		return nil, nil
   122  	}
   123  
   124  	if err := builder.Build(buildContext); err != nil {
   125  		return nil, errors.WithMessage(err, "external builder failed to build")
   126  	}
   127  
   128  	if err := builder.Release(buildContext); err != nil {
   129  		return nil, errors.WithMessage(err, "external builder failed to release")
   130  	}
   131  
   132  	durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid))
   133  
   134  	err = os.Mkdir(durablePath, 0700)
   135  	if err != nil {
   136  		return nil, errors.WithMessagef(err, "could not create dir '%s' to persist build output", durablePath)
   137  	}
   138  
   139  	buildInfo, err := json.Marshal(&BuildInfo{
   140  		BuilderName: builder.Name,
   141  	})
   142  	if err != nil {
   143  		os.RemoveAll(durablePath)
   144  		return nil, errors.WithMessage(err, "could not marshal for build-info.json")
   145  	}
   146  
   147  	err = ioutil.WriteFile(filepath.Join(durablePath, "build-info.json"), buildInfo, 0600)
   148  	if err != nil {
   149  		os.RemoveAll(durablePath)
   150  		return nil, errors.WithMessage(err, "could not write build-info.json")
   151  	}
   152  
   153  	durableReleaseDir := filepath.Join(durablePath, "release")
   154  	err = MoveOrCopyDir(logger, buildContext.ReleaseDir, durableReleaseDir)
   155  	if err != nil {
   156  		return nil, errors.WithMessagef(err, "could not move or copy build context release to persistent location '%s'", durablePath)
   157  	}
   158  
   159  	durableBldDir := filepath.Join(durablePath, "bld")
   160  	err = MoveOrCopyDir(logger, buildContext.BldDir, durableBldDir)
   161  	if err != nil {
   162  		return nil, errors.WithMessagef(err, "could not move or copy build context bld to persistent location '%s'", durablePath)
   163  	}
   164  
   165  	return &Instance{
   166  		PackageID:   ccid,
   167  		Builder:     builder,
   168  		BldDir:      durableBldDir,
   169  		ReleaseDir:  durableReleaseDir,
   170  		TermTimeout: 5 * time.Second,
   171  	}, nil
   172  }
   173  
   174  func (d *Detector) detect(buildContext *BuildContext) *Builder {
   175  	for _, builder := range d.Builders {
   176  		if builder.Detect(buildContext) {
   177  			return builder
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  // BuildContext holds references to the various assets locations necessary to
   184  // execute the detect, build, release, and run programs for external builders
   185  type BuildContext struct {
   186  	CCID        string
   187  	ScratchDir  string
   188  	SourceDir   string
   189  	ReleaseDir  string
   190  	MetadataDir string
   191  	BldDir      string
   192  }
   193  
   194  // NewBuildContext creates the directories required to runt he external
   195  // build process and extracts the chaincode package assets.
   196  //
   197  // Users of the BuildContext must call Cleanup when the build process is
   198  // complete to remove the transient file system assets.
   199  func NewBuildContext(ccid string, mdBytes []byte, codePackage io.Reader) (bc *BuildContext, err error) {
   200  	scratchDir, err := ioutil.TempDir("", "fabric-"+SanitizeCCIDPath(ccid))
   201  	if err != nil {
   202  		return nil, errors.WithMessage(err, "could not create temp dir")
   203  	}
   204  
   205  	defer func() {
   206  		if err != nil {
   207  			os.RemoveAll(scratchDir)
   208  		}
   209  	}()
   210  
   211  	sourceDir := filepath.Join(scratchDir, "src")
   212  	if err = os.Mkdir(sourceDir, 0700); err != nil {
   213  		return nil, errors.WithMessage(err, "could not create source dir")
   214  	}
   215  
   216  	metadataDir := filepath.Join(scratchDir, "metadata")
   217  	if err = os.Mkdir(metadataDir, 0700); err != nil {
   218  		return nil, errors.WithMessage(err, "could not create metadata dir")
   219  	}
   220  
   221  	outputDir := filepath.Join(scratchDir, "bld")
   222  	if err = os.Mkdir(outputDir, 0700); err != nil {
   223  		return nil, errors.WithMessage(err, "could not create build dir")
   224  	}
   225  
   226  	releaseDir := filepath.Join(scratchDir, "release")
   227  	if err = os.Mkdir(releaseDir, 0700); err != nil {
   228  		return nil, errors.WithMessage(err, "could not create release dir")
   229  	}
   230  
   231  	err = Untar(codePackage, sourceDir)
   232  	if err != nil {
   233  		return nil, errors.WithMessage(err, "could not untar source package")
   234  	}
   235  
   236  	err = ioutil.WriteFile(filepath.Join(metadataDir, "metadata.json"), mdBytes, 0700)
   237  	if err != nil {
   238  		return nil, errors.WithMessage(err, "could not write metadata file")
   239  	}
   240  
   241  	return &BuildContext{
   242  		ScratchDir:  scratchDir,
   243  		SourceDir:   sourceDir,
   244  		MetadataDir: metadataDir,
   245  		BldDir:      outputDir,
   246  		ReleaseDir:  releaseDir,
   247  		CCID:        ccid,
   248  	}, nil
   249  }
   250  
   251  // Cleanup removes the build context artifacts.
   252  func (bc *BuildContext) Cleanup() {
   253  	os.RemoveAll(bc.ScratchDir)
   254  }
   255  
   256  var pkgIDreg = regexp.MustCompile("[<>:\"/\\\\|\\?\\*&]")
   257  
   258  // SanitizeCCIDPath is used to ensure that special characters are removed from
   259  // file names.
   260  func SanitizeCCIDPath(ccid string) string {
   261  	return pkgIDreg.ReplaceAllString(ccid, "-")
   262  }
   263  
   264  // A Builder is used to interact with an external chaincode builder and launcher.
   265  type Builder struct {
   266  	EnvWhitelist []string
   267  	Location     string
   268  	Logger       *flogging.FabricLogger
   269  	Name         string
   270  }
   271  
   272  // CreateBuilders will construct builders from the peer configuration.
   273  func CreateBuilders(builderConfs []peer.ExternalBuilder) []*Builder {
   274  	var builders []*Builder
   275  	for _, builderConf := range builderConfs {
   276  		builders = append(builders, &Builder{
   277  			Location:     builderConf.Path,
   278  			Name:         builderConf.Name,
   279  			EnvWhitelist: builderConf.EnvironmentWhitelist,
   280  			Logger:       logger.Named(builderConf.Name),
   281  		})
   282  	}
   283  	return builders
   284  }
   285  
   286  // Detect runs the `detect` script.
   287  func (b *Builder) Detect(buildContext *BuildContext) bool {
   288  	detect := filepath.Join(b.Location, "bin", "detect")
   289  	cmd := b.NewCommand(detect, buildContext.SourceDir, buildContext.MetadataDir)
   290  
   291  	err := b.runCommand(cmd)
   292  	if err != nil {
   293  		logger.Debugf("builder '%s' detect failed: %s", b.Name, err)
   294  		return false
   295  	}
   296  
   297  	return true
   298  }
   299  
   300  // Build runs the `build` script.
   301  func (b *Builder) Build(buildContext *BuildContext) error {
   302  	build := filepath.Join(b.Location, "bin", "build")
   303  	cmd := b.NewCommand(build, buildContext.SourceDir, buildContext.MetadataDir, buildContext.BldDir)
   304  
   305  	err := b.runCommand(cmd)
   306  	if err != nil {
   307  		return errors.Wrapf(err, "external builder '%s' failed", b.Name)
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  // Release runs the `release` script.
   314  func (b *Builder) Release(buildContext *BuildContext) error {
   315  	release := filepath.Join(b.Location, "bin", "release")
   316  
   317  	_, err := os.Stat(release)
   318  	if os.IsNotExist(err) {
   319  		b.Logger.Debugf("Skipping release step for '%s' as no release binary found", buildContext.CCID)
   320  		return nil
   321  	}
   322  	if err != nil {
   323  		return errors.WithMessagef(err, "could not stat release binary '%s'", release)
   324  	}
   325  
   326  	cmd := b.NewCommand(release, buildContext.BldDir, buildContext.ReleaseDir)
   327  	err = b.runCommand(cmd)
   328  	if err != nil {
   329  		return errors.Wrapf(err, "builder '%s' release failed", b.Name)
   330  	}
   331  
   332  	return nil
   333  }
   334  
   335  // runConfig is serialized to disk when launching.
   336  type runConfig struct {
   337  	CCID        string `json:"chaincode_id"`
   338  	PeerAddress string `json:"peer_address"`
   339  	ClientCert  string `json:"client_cert"` // PEM encoded client certificate
   340  	ClientKey   string `json:"client_key"`  // PEM encoded client key
   341  	RootCert    string `json:"root_cert"`   // PEM encoded peer chaincode certificate
   342  }
   343  
   344  func newRunConfig(ccid string, peerConnection *ccintf.PeerConnection) runConfig {
   345  	var tlsConfig ccintf.TLSConfig
   346  	if peerConnection.TLSConfig != nil {
   347  		tlsConfig = *peerConnection.TLSConfig
   348  	}
   349  
   350  	return runConfig{
   351  		PeerAddress: peerConnection.Address,
   352  		CCID:        ccid,
   353  		ClientCert:  string(tlsConfig.ClientCert),
   354  		ClientKey:   string(tlsConfig.ClientKey),
   355  		RootCert:    string(tlsConfig.RootCert),
   356  	}
   357  }
   358  
   359  // Run starts the `run` script and returns a Session that can be used to
   360  // signal it and wait for termination.
   361  func (b *Builder) Run(ccid, bldDir string, peerConnection *ccintf.PeerConnection) (*Session, error) {
   362  	launchDir, err := ioutil.TempDir("", "fabric-run")
   363  	if err != nil {
   364  		return nil, errors.WithMessage(err, "could not create temp run dir")
   365  	}
   366  
   367  	rc := newRunConfig(ccid, peerConnection)
   368  	marshaledRC, err := json.Marshal(rc)
   369  	if err != nil {
   370  		return nil, errors.WithMessage(err, "could not marshal run config")
   371  	}
   372  
   373  	if err := ioutil.WriteFile(filepath.Join(launchDir, "chaincode.json"), marshaledRC, 0600); err != nil {
   374  		return nil, errors.WithMessage(err, "could not write root cert")
   375  	}
   376  
   377  	run := filepath.Join(b.Location, "bin", "run")
   378  	cmd := b.NewCommand(run, bldDir, launchDir)
   379  	sess, err := Start(b.Logger, cmd)
   380  	if err != nil {
   381  		os.RemoveAll(launchDir)
   382  		return nil, errors.Wrapf(err, "builder '%s' run failed to start", b.Name)
   383  	}
   384  
   385  	go func() {
   386  		defer os.RemoveAll(launchDir)
   387  		sess.Wait()
   388  	}()
   389  
   390  	return sess, nil
   391  }
   392  
   393  // runCommand runs a command and waits for it to complete.
   394  func (b *Builder) runCommand(cmd *exec.Cmd) error {
   395  	sess, err := Start(b.Logger, cmd)
   396  	if err != nil {
   397  		return err
   398  	}
   399  	return sess.Wait()
   400  }
   401  
   402  // NewCommand creates an exec.Cmd that is configured to prune the calling
   403  // environment down to the environment variables specified in the external
   404  // builder's EnvironmentWhitelist and the DefaultEnvWhitelist.
   405  func (b *Builder) NewCommand(name string, args ...string) *exec.Cmd {
   406  	cmd := exec.Command(name, args...)
   407  	whitelist := appendDefaultWhitelist(b.EnvWhitelist)
   408  	for _, key := range whitelist {
   409  		if val, ok := os.LookupEnv(key); ok {
   410  			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
   411  		}
   412  	}
   413  	return cmd
   414  }
   415  
   416  func appendDefaultWhitelist(envWhitelist []string) []string {
   417  	for _, variable := range DefaultEnvWhitelist {
   418  		if !contains(envWhitelist, variable) {
   419  			envWhitelist = append(envWhitelist, variable)
   420  		}
   421  	}
   422  	return envWhitelist
   423  }
   424  
   425  func contains(envWhiteList []string, key string) bool {
   426  	for _, variable := range envWhiteList {
   427  		if key == variable {
   428  			return true
   429  		}
   430  	}
   431  	return false
   432  }