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