429 lines
13 KiB
Go
429 lines
13 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package externalbuilder
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/hyperledger/fabric/common/flogging"
|
|
"github.com/hyperledger/fabric/core/container/ccintf"
|
|
"github.com/hyperledger/fabric/core/peer"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
// DefaultPropagateEnvironment enumerates the list of environment variables that are
|
|
// implicitly propagated to external builder and launcher commands.
|
|
DefaultPropagateEnvironment = []string{"LD_LIBRARY_PATH", "LIBPATH", "PATH", "TMPDIR"}
|
|
|
|
logger = flogging.MustGetLogger("chaincode.externalbuilder")
|
|
)
|
|
|
|
// BuildInfo contains metadata is that is saved to the local file system with the
|
|
// assets generated by an external builder. This is used to associate build output
|
|
// with the builder that generated it.
|
|
type BuildInfo struct {
|
|
// BuilderName is the user provided name of the external builder.
|
|
BuilderName string `json:"builder_name"`
|
|
}
|
|
|
|
// A Detector is responsible for orchestrating the external builder detection and
|
|
// build process.
|
|
type Detector struct {
|
|
// DurablePath is the file system location where chaincode assets are persisted.
|
|
DurablePath string
|
|
// Builders are the builders that detect and build processing will use.
|
|
Builders []*Builder
|
|
}
|
|
|
|
// CachedBuild returns a build instance that was already built or nil when no
|
|
// instance has been found. An error is returned only when an unexpected
|
|
// condition is encountered.
|
|
func (d *Detector) CachedBuild(ccid string) (*Instance, error) {
|
|
durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid))
|
|
_, err := os.Stat(durablePath)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "existing build detected, but something went wrong inspecting it")
|
|
}
|
|
|
|
buildInfoPath := filepath.Join(durablePath, "build-info.json")
|
|
buildInfoData, err := ioutil.ReadFile(buildInfoPath)
|
|
if err != nil {
|
|
return nil, errors.WithMessagef(err, "could not read '%s' for build info", buildInfoPath)
|
|
}
|
|
|
|
var buildInfo BuildInfo
|
|
if err := json.Unmarshal(buildInfoData, &buildInfo); err != nil {
|
|
return nil, errors.WithMessagef(err, "malformed build info at '%s'", buildInfoPath)
|
|
}
|
|
|
|
for _, builder := range d.Builders {
|
|
if builder.Name == buildInfo.BuilderName {
|
|
return &Instance{
|
|
PackageID: ccid,
|
|
Builder: builder,
|
|
BldDir: filepath.Join(durablePath, "bld"),
|
|
ReleaseDir: filepath.Join(durablePath, "release"),
|
|
TermTimeout: 5 * time.Second,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Errorf("chaincode '%s' was already built with builder '%s', but that builder is no longer available", ccid, buildInfo.BuilderName)
|
|
}
|
|
|
|
// Build executes the external builder detect and build process.
|
|
//
|
|
// Before running the detect and build process, the detector first checks the
|
|
// durable path for the results of a previous build for the provided package.
|
|
// If found, the detect and build process is skipped and the existing instance
|
|
// is returned.
|
|
func (d *Detector) Build(ccid string, mdBytes []byte, codeStream io.Reader) (*Instance, error) {
|
|
// A small optimization: prevent exploding the build package out into the
|
|
// file system unless there are external builders defined.
|
|
if len(d.Builders) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Look for a cached instance.
|
|
i, err := d.CachedBuild(ccid)
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "existing build could not be restored")
|
|
}
|
|
if i != nil {
|
|
return i, nil
|
|
}
|
|
|
|
buildContext, err := NewBuildContext(ccid, mdBytes, codeStream)
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not create build context")
|
|
}
|
|
defer buildContext.Cleanup()
|
|
|
|
builder := d.detect(buildContext)
|
|
if builder == nil {
|
|
logger.Debugf("no external builder detected for %s", ccid)
|
|
return nil, nil
|
|
}
|
|
|
|
if err := builder.Build(buildContext); err != nil {
|
|
return nil, errors.WithMessage(err, "external builder failed to build")
|
|
}
|
|
|
|
if err := builder.Release(buildContext); err != nil {
|
|
return nil, errors.WithMessage(err, "external builder failed to release")
|
|
}
|
|
|
|
durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid))
|
|
|
|
err = os.Mkdir(durablePath, 0o700)
|
|
if err != nil {
|
|
return nil, errors.WithMessagef(err, "could not create dir '%s' to persist build output", durablePath)
|
|
}
|
|
|
|
buildInfo, err := json.Marshal(&BuildInfo{
|
|
BuilderName: builder.Name,
|
|
})
|
|
if err != nil {
|
|
os.RemoveAll(durablePath)
|
|
return nil, errors.WithMessage(err, "could not marshal for build-info.json")
|
|
}
|
|
|
|
err = ioutil.WriteFile(filepath.Join(durablePath, "build-info.json"), buildInfo, 0o600)
|
|
if err != nil {
|
|
os.RemoveAll(durablePath)
|
|
return nil, errors.WithMessage(err, "could not write build-info.json")
|
|
}
|
|
|
|
durableReleaseDir := filepath.Join(durablePath, "release")
|
|
err = CopyDir(logger, buildContext.ReleaseDir, durableReleaseDir)
|
|
if err != nil {
|
|
return nil, errors.WithMessagef(err, "could not move or copy build context release to persistent location '%s'", durablePath)
|
|
}
|
|
|
|
durableBldDir := filepath.Join(durablePath, "bld")
|
|
err = CopyDir(logger, buildContext.BldDir, durableBldDir)
|
|
if err != nil {
|
|
return nil, errors.WithMessagef(err, "could not move or copy build context bld to persistent location '%s'", durablePath)
|
|
}
|
|
|
|
return &Instance{
|
|
PackageID: ccid,
|
|
Builder: builder,
|
|
BldDir: durableBldDir,
|
|
ReleaseDir: durableReleaseDir,
|
|
TermTimeout: 5 * time.Second,
|
|
}, nil
|
|
}
|
|
|
|
func (d *Detector) detect(buildContext *BuildContext) *Builder {
|
|
for _, builder := range d.Builders {
|
|
if builder.Detect(buildContext) {
|
|
return builder
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildContext holds references to the various assets locations necessary to
|
|
// execute the detect, build, release, and run programs for external builders
|
|
type BuildContext struct {
|
|
CCID string
|
|
ScratchDir string
|
|
SourceDir string
|
|
ReleaseDir string
|
|
MetadataDir string
|
|
BldDir string
|
|
}
|
|
|
|
// NewBuildContext creates the directories required to runt he external
|
|
// build process and extracts the chaincode package assets.
|
|
//
|
|
// Users of the BuildContext must call Cleanup when the build process is
|
|
// complete to remove the transient file system assets.
|
|
func NewBuildContext(ccid string, mdBytes []byte, codePackage io.Reader) (bc *BuildContext, err error) {
|
|
scratchDir, err := ioutil.TempDir("", "fabric-"+SanitizeCCIDPath(ccid))
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not create temp dir")
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
os.RemoveAll(scratchDir)
|
|
}
|
|
}()
|
|
|
|
sourceDir := filepath.Join(scratchDir, "src")
|
|
if err = os.Mkdir(sourceDir, 0o700); err != nil {
|
|
return nil, errors.WithMessage(err, "could not create source dir")
|
|
}
|
|
|
|
metadataDir := filepath.Join(scratchDir, "metadata")
|
|
if err = os.Mkdir(metadataDir, 0o700); err != nil {
|
|
return nil, errors.WithMessage(err, "could not create metadata dir")
|
|
}
|
|
|
|
outputDir := filepath.Join(scratchDir, "bld")
|
|
if err = os.Mkdir(outputDir, 0o700); err != nil {
|
|
return nil, errors.WithMessage(err, "could not create build dir")
|
|
}
|
|
|
|
releaseDir := filepath.Join(scratchDir, "release")
|
|
if err = os.Mkdir(releaseDir, 0o700); err != nil {
|
|
return nil, errors.WithMessage(err, "could not create release dir")
|
|
}
|
|
|
|
err = Untar(codePackage, sourceDir)
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not untar source package")
|
|
}
|
|
|
|
err = ioutil.WriteFile(filepath.Join(metadataDir, "metadata.json"), mdBytes, 0o700)
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not write metadata file")
|
|
}
|
|
|
|
return &BuildContext{
|
|
ScratchDir: scratchDir,
|
|
SourceDir: sourceDir,
|
|
MetadataDir: metadataDir,
|
|
BldDir: outputDir,
|
|
ReleaseDir: releaseDir,
|
|
CCID: ccid,
|
|
}, nil
|
|
}
|
|
|
|
// Cleanup removes the build context artifacts.
|
|
func (bc *BuildContext) Cleanup() {
|
|
os.RemoveAll(bc.ScratchDir)
|
|
}
|
|
|
|
var pkgIDreg = regexp.MustCompile("[<>:\"/\\\\|\\?\\*&]")
|
|
|
|
// SanitizeCCIDPath is used to ensure that special characters are removed from
|
|
// file names.
|
|
func SanitizeCCIDPath(ccid string) string {
|
|
return pkgIDreg.ReplaceAllString(ccid, "-")
|
|
}
|
|
|
|
// A Builder is used to interact with an external chaincode builder and launcher.
|
|
type Builder struct {
|
|
PropagateEnvironment []string
|
|
Location string
|
|
Logger *flogging.FabricLogger
|
|
Name string
|
|
MSPID string
|
|
}
|
|
|
|
// CreateBuilders will construct builders from the peer configuration.
|
|
func CreateBuilders(builderConfs []peer.ExternalBuilder, mspid string) []*Builder {
|
|
var builders []*Builder
|
|
for _, builderConf := range builderConfs {
|
|
builders = append(builders, &Builder{
|
|
Location: builderConf.Path,
|
|
Name: builderConf.Name,
|
|
PropagateEnvironment: builderConf.PropagateEnvironment,
|
|
Logger: logger.Named(builderConf.Name),
|
|
MSPID: mspid,
|
|
})
|
|
}
|
|
return builders
|
|
}
|
|
|
|
// Detect runs the `detect` script.
|
|
func (b *Builder) Detect(buildContext *BuildContext) bool {
|
|
detect := filepath.Join(b.Location, "bin", "detect")
|
|
cmd := b.NewCommand(detect, buildContext.SourceDir, buildContext.MetadataDir)
|
|
|
|
err := b.runCommand(cmd)
|
|
if err != nil {
|
|
logger.Debugf("builder '%s' detect failed: %s", b.Name, err)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Build runs the `build` script.
|
|
func (b *Builder) Build(buildContext *BuildContext) error {
|
|
build := filepath.Join(b.Location, "bin", "build")
|
|
cmd := b.NewCommand(build, buildContext.SourceDir, buildContext.MetadataDir, buildContext.BldDir)
|
|
|
|
err := b.runCommand(cmd)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "external builder '%s' failed", b.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Release runs the `release` script.
|
|
func (b *Builder) Release(buildContext *BuildContext) error {
|
|
release := filepath.Join(b.Location, "bin", "release")
|
|
|
|
_, err := exec.LookPath(release)
|
|
if err != nil {
|
|
b.Logger.Debugf("Skipping release step for '%s' as no release binary found", buildContext.CCID)
|
|
return nil
|
|
}
|
|
|
|
cmd := b.NewCommand(release, buildContext.BldDir, buildContext.ReleaseDir)
|
|
err = b.runCommand(cmd)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "builder '%s' release failed", b.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runConfig is serialized to disk when launching.
|
|
type runConfig struct {
|
|
CCID string `json:"chaincode_id"`
|
|
PeerAddress string `json:"peer_address"`
|
|
ClientCert string `json:"client_cert"` // PEM encoded client certificate
|
|
ClientKey string `json:"client_key"` // PEM encoded client key
|
|
RootCert string `json:"root_cert"` // PEM encoded peer chaincode certificate
|
|
MSPID string `json:"mspid"`
|
|
}
|
|
|
|
func newRunConfig(ccid string, peerConnection *ccintf.PeerConnection, mspid string) runConfig {
|
|
var tlsConfig ccintf.TLSConfig
|
|
if peerConnection.TLSConfig != nil {
|
|
tlsConfig = *peerConnection.TLSConfig
|
|
}
|
|
|
|
return runConfig{
|
|
PeerAddress: peerConnection.Address,
|
|
CCID: ccid,
|
|
ClientCert: string(tlsConfig.ClientCert),
|
|
ClientKey: string(tlsConfig.ClientKey),
|
|
RootCert: string(tlsConfig.RootCert),
|
|
MSPID: mspid,
|
|
}
|
|
}
|
|
|
|
// Run starts the `run` script and returns a Session that can be used to
|
|
// signal it and wait for termination.
|
|
func (b *Builder) Run(ccid, bldDir string, peerConnection *ccintf.PeerConnection) (*Session, error) {
|
|
launchDir, err := ioutil.TempDir("", "fabric-run")
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not create temp run dir")
|
|
}
|
|
|
|
rc := newRunConfig(ccid, peerConnection, b.MSPID)
|
|
marshaledRC, err := json.Marshal(rc)
|
|
if err != nil {
|
|
return nil, errors.WithMessage(err, "could not marshal run config")
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filepath.Join(launchDir, "chaincode.json"), marshaledRC, 0o600); err != nil {
|
|
return nil, errors.WithMessage(err, "could not write root cert")
|
|
}
|
|
|
|
run := filepath.Join(b.Location, "bin", "run")
|
|
cmd := b.NewCommand(run, bldDir, launchDir)
|
|
sess, err := Start(b.Logger, cmd, func(error) { os.RemoveAll(launchDir) })
|
|
if err != nil {
|
|
os.RemoveAll(launchDir)
|
|
return nil, errors.Wrapf(err, "builder '%s' run failed to start", b.Name)
|
|
}
|
|
|
|
return sess, nil
|
|
}
|
|
|
|
// runCommand runs a command and waits for it to complete.
|
|
func (b *Builder) runCommand(cmd *exec.Cmd) error {
|
|
sess, err := Start(b.Logger, cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sess.Wait()
|
|
}
|
|
|
|
// NewCommand creates an exec.Cmd that is configured to prune the calling
|
|
// environment down to the environment variables specified in the external
|
|
// builder's PropagateEnvironment and the DefaultPropagateEnvironment.
|
|
func (b *Builder) NewCommand(name string, args ...string) *exec.Cmd {
|
|
cmd := exec.Command(name, args...)
|
|
propagationList := appendDefaultPropagateEnvironment(b.PropagateEnvironment)
|
|
for _, key := range propagationList {
|
|
if val, ok := os.LookupEnv(key); ok {
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
|
|
}
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func appendDefaultPropagateEnvironment(propagateEnvironment []string) []string {
|
|
for _, variable := range DefaultPropagateEnvironment {
|
|
if !contains(propagateEnvironment, variable) {
|
|
propagateEnvironment = append(propagateEnvironment, variable)
|
|
}
|
|
}
|
|
return propagateEnvironment
|
|
}
|
|
|
|
func contains(propagateEnvironment []string, key string) bool {
|
|
for _, variable := range propagateEnvironment {
|
|
if key == variable {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|