/* 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 }