github.com/ahlemtn/fabric@v2.1.1+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 = CopyDir(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 = CopyDir(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  	MSPID        string
   271  }
   272  
   273  // CreateBuilders will construct builders from the peer configuration.
   274  func CreateBuilders(builderConfs []peer.ExternalBuilder, mspid string) []*Builder {
   275  	var builders []*Builder
   276  	for _, builderConf := range builderConfs {
   277  		builders = append(builders, &Builder{
   278  			Location:     builderConf.Path,
   279  			Name:         builderConf.Name,
   280  			EnvWhitelist: builderConf.EnvironmentWhitelist,
   281  			Logger:       logger.Named(builderConf.Name),
   282  			MSPID:        mspid,
   283  		})
   284  	}
   285  	return builders
   286  }
   287  
   288  // Detect runs the `detect` script.
   289  func (b *Builder) Detect(buildContext *BuildContext) bool {
   290  	detect := filepath.Join(b.Location, "bin", "detect")
   291  	cmd := b.NewCommand(detect, buildContext.SourceDir, buildContext.MetadataDir)
   292  
   293  	err := b.runCommand(cmd)
   294  	if err != nil {
   295  		logger.Debugf("builder '%s' detect failed: %s", b.Name, err)
   296  		return false
   297  	}
   298  
   299  	return true
   300  }
   301  
   302  // Build runs the `build` script.
   303  func (b *Builder) Build(buildContext *BuildContext) error {
   304  	build := filepath.Join(b.Location, "bin", "build")
   305  	cmd := b.NewCommand(build, buildContext.SourceDir, buildContext.MetadataDir, buildContext.BldDir)
   306  
   307  	err := b.runCommand(cmd)
   308  	if err != nil {
   309  		return errors.Wrapf(err, "external builder '%s' failed", b.Name)
   310  	}
   311  
   312  	return nil
   313  }
   314  
   315  // Release runs the `release` script.
   316  func (b *Builder) Release(buildContext *BuildContext) error {
   317  	release := filepath.Join(b.Location, "bin", "release")
   318  
   319  	_, err := exec.LookPath(release)
   320  	if err != nil {
   321  		b.Logger.Debugf("Skipping release step for '%s' as no release binary found", buildContext.CCID)
   322  		return nil
   323  	}
   324  
   325  	cmd := b.NewCommand(release, buildContext.BldDir, buildContext.ReleaseDir)
   326  	err = b.runCommand(cmd)
   327  	if err != nil {
   328  		return errors.Wrapf(err, "builder '%s' release failed", b.Name)
   329  	}
   330  
   331  	return nil
   332  }
   333  
   334  // runConfig is serialized to disk when launching.
   335  type runConfig struct {
   336  	CCID        string `json:"chaincode_id"`
   337  	PeerAddress string `json:"peer_address"`
   338  	ClientCert  string `json:"client_cert"` // PEM encoded client certificate
   339  	ClientKey   string `json:"client_key"`  // PEM encoded client key
   340  	RootCert    string `json:"root_cert"`   // PEM encoded peer chaincode certificate
   341  	MSPID       string `json:"mspid"`
   342  }
   343  
   344  func newRunConfig(ccid string, peerConnection *ccintf.PeerConnection, mspid string) 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  		MSPID:       mspid,
   357  	}
   358  }
   359  
   360  // Run starts the `run` script and returns a Session that can be used to
   361  // signal it and wait for termination.
   362  func (b *Builder) Run(ccid, bldDir string, peerConnection *ccintf.PeerConnection) (*Session, error) {
   363  	launchDir, err := ioutil.TempDir("", "fabric-run")
   364  	if err != nil {
   365  		return nil, errors.WithMessage(err, "could not create temp run dir")
   366  	}
   367  
   368  	rc := newRunConfig(ccid, peerConnection, b.MSPID)
   369  	marshaledRC, err := json.Marshal(rc)
   370  	if err != nil {
   371  		return nil, errors.WithMessage(err, "could not marshal run config")
   372  	}
   373  
   374  	if err := ioutil.WriteFile(filepath.Join(launchDir, "chaincode.json"), marshaledRC, 0600); err != nil {
   375  		return nil, errors.WithMessage(err, "could not write root cert")
   376  	}
   377  
   378  	run := filepath.Join(b.Location, "bin", "run")
   379  	cmd := b.NewCommand(run, bldDir, launchDir)
   380  	sess, err := Start(b.Logger, cmd)
   381  	if err != nil {
   382  		os.RemoveAll(launchDir)
   383  		return nil, errors.Wrapf(err, "builder '%s' run failed to start", b.Name)
   384  	}
   385  
   386  	go func() {
   387  		defer os.RemoveAll(launchDir)
   388  		sess.Wait()
   389  	}()
   390  
   391  	return sess, nil
   392  }
   393  
   394  // runCommand runs a command and waits for it to complete.
   395  func (b *Builder) runCommand(cmd *exec.Cmd) error {
   396  	sess, err := Start(b.Logger, cmd)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	return sess.Wait()
   401  }
   402  
   403  // NewCommand creates an exec.Cmd that is configured to prune the calling
   404  // environment down to the environment variables specified in the external
   405  // builder's EnvironmentWhitelist and the DefaultEnvWhitelist.
   406  func (b *Builder) NewCommand(name string, args ...string) *exec.Cmd {
   407  	cmd := exec.Command(name, args...)
   408  	whitelist := appendDefaultWhitelist(b.EnvWhitelist)
   409  	for _, key := range whitelist {
   410  		if val, ok := os.LookupEnv(key); ok {
   411  			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
   412  		}
   413  	}
   414  	return cmd
   415  }
   416  
   417  func appendDefaultWhitelist(envWhitelist []string) []string {
   418  	for _, variable := range DefaultEnvWhitelist {
   419  		if !contains(envWhitelist, variable) {
   420  			envWhitelist = append(envWhitelist, variable)
   421  		}
   422  	}
   423  	return envWhitelist
   424  }
   425  
   426  func contains(envWhiteList []string, key string) bool {
   427  	for _, variable := range envWhiteList {
   428  		if key == variable {
   429  			return true
   430  		}
   431  	}
   432  	return false
   433  }