github.com/hyperledger-labs/bdls@v2.1.1+incompatible/core/chaincode/platforms/golang/platform.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package golang
     8  
     9  import (
    10  	"archive/tar"
    11  	"bytes"
    12  	"compress/gzip"
    13  	"fmt"
    14  	"io"
    15  	"io/ioutil"
    16  	"os"
    17  	"os/exec"
    18  	"path"
    19  	"path/filepath"
    20  	"regexp"
    21  	"runtime"
    22  	"sort"
    23  	"strings"
    24  
    25  	pb "github.com/hyperledger/fabric-protos-go/peer"
    26  	"github.com/hyperledger/fabric/core/chaincode/platforms/util"
    27  	"github.com/hyperledger/fabric/internal/ccmetadata"
    28  	"github.com/pkg/errors"
    29  	"github.com/spf13/viper"
    30  )
    31  
    32  // Platform for chaincodes written in Go
    33  type Platform struct{}
    34  
    35  // Name returns the name of this platform.
    36  func (p *Platform) Name() string {
    37  	return pb.ChaincodeSpec_GOLANG.String()
    38  }
    39  
    40  // ValidatePath is used to ensure that path provided points to something that
    41  // looks like go chainccode.
    42  //
    43  // NOTE: this is only used at the _client_ side by the peer CLI.
    44  func (p *Platform) ValidatePath(rawPath string) error {
    45  	_, err := DescribeCode(rawPath)
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  // NormalizePath is used to extract a relative module path from a module root.
    54  // This should not impact legacy GOPATH chaincode.
    55  //
    56  // NOTE: this is only used at the _client_ side by the peer CLI.
    57  func (p *Platform) NormalizePath(rawPath string) (string, error) {
    58  	modInfo, err := moduleInfo(rawPath)
    59  	if err != nil {
    60  		return "", err
    61  	}
    62  
    63  	// not a module
    64  	if modInfo == nil {
    65  		return rawPath, nil
    66  	}
    67  
    68  	return modInfo.ImportPath, nil
    69  }
    70  
    71  // ValidateCodePackage examines the chaincode archive to ensure it is valid.
    72  //
    73  // NOTE: this code is used in some transaction validation paths but can be changed
    74  // post 2.0.
    75  func (p *Platform) ValidateCodePackage(code []byte) error {
    76  	is := bytes.NewReader(code)
    77  	gr, err := gzip.NewReader(is)
    78  	if err != nil {
    79  		return fmt.Errorf("failure opening codepackage gzip stream: %s", err)
    80  	}
    81  
    82  	re := regexp.MustCompile(`^(src|META-INF)/`)
    83  	tr := tar.NewReader(gr)
    84  	for {
    85  		header, err := tr.Next()
    86  		if err == io.EOF {
    87  			break
    88  		}
    89  		if err != nil {
    90  			return err
    91  		}
    92  
    93  		// maintain check for conforming paths for validation
    94  		if !re.MatchString(header.Name) {
    95  			return fmt.Errorf("illegal file name in payload: %s", header.Name)
    96  		}
    97  
    98  		// only files and directories; no links or special files
    99  		mode := header.FileInfo().Mode()
   100  		if mode&^(os.ModeDir|0777) != 0 {
   101  			return fmt.Errorf("illegal file mode in payload: %s", header.Name)
   102  		}
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // Directory constant copied from tar package.
   109  const c_ISDIR = 040000
   110  
   111  // Default compression to use for production. Test packages disable compression.
   112  var gzipCompressionLevel = gzip.DefaultCompression
   113  
   114  // GetDeploymentPayload creates a gzip compressed tape archive that contains the
   115  // required assets to build and run go chaincode.
   116  //
   117  // NOTE: this is only used at the _client_ side by the peer CLI.
   118  func (p *Platform) GetDeploymentPayload(codepath string) ([]byte, error) {
   119  	codeDescriptor, err := DescribeCode(codepath)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	fileMap, err := findSource(codeDescriptor)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	var dependencyPackageInfo []PackageInfo
   130  	if !codeDescriptor.Module {
   131  		for _, dist := range distributions() {
   132  			pi, err := gopathDependencyPackageInfo(dist.goos, dist.goarch, codeDescriptor.Path)
   133  			if err != nil {
   134  				return nil, err
   135  			}
   136  			dependencyPackageInfo = append(dependencyPackageInfo, pi...)
   137  		}
   138  	}
   139  
   140  	for _, pkg := range dependencyPackageInfo {
   141  		for _, filename := range pkg.Files() {
   142  			sd := SourceDescriptor{
   143  				Name: path.Join("src", pkg.ImportPath, filename),
   144  				Path: filepath.Join(pkg.Dir, filename),
   145  			}
   146  			fileMap[sd.Name] = sd
   147  		}
   148  	}
   149  
   150  	payload := bytes.NewBuffer(nil)
   151  	gw, err := gzip.NewWriterLevel(payload, gzipCompressionLevel)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	tw := tar.NewWriter(gw)
   156  
   157  	// Create directories so they get sane ownership and permissions
   158  	for _, dirname := range fileMap.Directories() {
   159  		err := tw.WriteHeader(&tar.Header{
   160  			Typeflag: tar.TypeDir,
   161  			Name:     dirname + "/",
   162  			Mode:     c_ISDIR | 0755,
   163  			Uid:      500,
   164  			Gid:      500,
   165  		})
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	for _, file := range fileMap.Sources() {
   172  		err = util.WriteFileToPackage(file.Path, file.Name, tw)
   173  		if err != nil {
   174  			return nil, fmt.Errorf("Error writing %s to tar: %s", file.Name, err)
   175  		}
   176  	}
   177  
   178  	err = tw.Close()
   179  	if err == nil {
   180  		err = gw.Close()
   181  	}
   182  	if err != nil {
   183  		return nil, errors.Wrapf(err, "failed to create tar for chaincode")
   184  	}
   185  
   186  	return payload.Bytes(), nil
   187  }
   188  
   189  func (p *Platform) GenerateDockerfile() (string, error) {
   190  	var buf []string
   191  	buf = append(buf, "FROM "+util.GetDockerImageFromConfig("chaincode.golang.runtime"))
   192  	buf = append(buf, "ADD binpackage.tar /usr/local/bin")
   193  
   194  	return strings.Join(buf, "\n"), nil
   195  }
   196  
   197  const staticLDFlagsOpts = "-ldflags \"-linkmode external -extldflags '-static'\""
   198  const dynamicLDFlagsOpts = ""
   199  
   200  func getLDFlagsOpts() string {
   201  	if viper.GetBool("chaincode.golang.dynamicLink") {
   202  		return dynamicLDFlagsOpts
   203  	}
   204  	return staticLDFlagsOpts
   205  }
   206  
   207  var buildScript = `
   208  set -e
   209  if [ -f "/chaincode/input/src/go.mod" ] && [ -d "/chaincode/input/src/vendor" ]; then
   210      cd /chaincode/input/src
   211      GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode %[2]s
   212  elif [ -f "/chaincode/input/src/go.mod" ]; then
   213      cd /chaincode/input/src
   214      GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode %[2]s
   215  elif [ -f "/chaincode/input/src/%[2]s/go.mod" ] && [ -d "/chaincode/input/src/%[2]s/vendor" ]; then
   216      cd /chaincode/input/src/%[2]s
   217      GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode .
   218  elif [ -f "/chaincode/input/src/%[2]s/go.mod" ]; then
   219      cd /chaincode/input/src/%[2]s
   220      GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode .
   221  else
   222      GOPATH=/chaincode/input:$GOPATH go build -v %[1]s -o /chaincode/output/chaincode %[2]s
   223  fi
   224  echo Done!
   225  `
   226  
   227  func (p *Platform) DockerBuildOptions(path string) (util.DockerBuildOptions, error) {
   228  	env := []string{}
   229  	for _, key := range []string{"GOPROXY", "GOSUMDB"} {
   230  		if val, ok := os.LookupEnv(key); ok {
   231  			env = append(env, fmt.Sprintf("%s=%s", key, val))
   232  			continue
   233  		}
   234  		if key == "GOPROXY" {
   235  			env = append(env, "GOPROXY=https://proxy.golang.org")
   236  		}
   237  	}
   238  	ldFlagOpts := getLDFlagsOpts()
   239  	return util.DockerBuildOptions{
   240  		Cmd: fmt.Sprintf(buildScript, ldFlagOpts, path),
   241  		Env: env,
   242  	}, nil
   243  }
   244  
   245  // CodeDescriptor describes the code we're packaging.
   246  type CodeDescriptor struct {
   247  	Source       string // absolute path of the source to package
   248  	MetadataRoot string // absolute path META-INF
   249  	Path         string // import path of the package
   250  	Module       bool   // does this represent a go module
   251  }
   252  
   253  func (cd CodeDescriptor) isMetadata(path string) bool {
   254  	return strings.HasPrefix(
   255  		filepath.Clean(path),
   256  		filepath.Clean(cd.MetadataRoot),
   257  	)
   258  }
   259  
   260  // DescribeCode returns GOPATH and package information.
   261  func DescribeCode(path string) (*CodeDescriptor, error) {
   262  	if path == "" {
   263  		return nil, errors.New("cannot collect files from empty chaincode path")
   264  	}
   265  
   266  	// Use the module root as the source path for go modules
   267  	modInfo, err := moduleInfo(path)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	if modInfo != nil {
   273  		// calculate where the metadata should be relative to module root
   274  		relImport, err := filepath.Rel(modInfo.ModulePath, modInfo.ImportPath)
   275  		if err != nil {
   276  			return nil, err
   277  		}
   278  
   279  		return &CodeDescriptor{
   280  			Module:       true,
   281  			MetadataRoot: filepath.Join(modInfo.Dir, relImport, "META-INF"),
   282  			Path:         modInfo.ImportPath,
   283  			Source:       modInfo.Dir,
   284  		}, nil
   285  	}
   286  
   287  	return describeGopath(path)
   288  }
   289  
   290  func describeGopath(importPath string) (*CodeDescriptor, error) {
   291  	output, err := exec.Command("go", "list", "-f", "{{.Dir}}", importPath).Output()
   292  	if err != nil {
   293  		return nil, wrapExitErr(err, "'go list' failed")
   294  	}
   295  	sourcePath := filepath.Clean(strings.TrimSpace(string(output)))
   296  
   297  	return &CodeDescriptor{
   298  		Path:         importPath,
   299  		MetadataRoot: filepath.Join(sourcePath, "META-INF"),
   300  		Source:       sourcePath,
   301  	}, nil
   302  }
   303  
   304  func regularFileExists(path string) (bool, error) {
   305  	fi, err := os.Stat(path)
   306  	switch {
   307  	case os.IsNotExist(err):
   308  		return false, nil
   309  	case err != nil:
   310  		return false, err
   311  	default:
   312  		return fi.Mode().IsRegular(), nil
   313  	}
   314  }
   315  
   316  func moduleInfo(path string) (*ModuleInfo, error) {
   317  	entryWD, err := os.Getwd()
   318  	if err != nil {
   319  		return nil, errors.Wrap(err, "failed to get working directory")
   320  	}
   321  
   322  	// directory doesn't exist so unlikely to be a module
   323  	if err := os.Chdir(path); err != nil {
   324  		return nil, nil
   325  	}
   326  	defer func() {
   327  		if err := os.Chdir(entryWD); err != nil {
   328  			panic(fmt.Sprintf("failed to restore working directory: %s", err))
   329  		}
   330  	}()
   331  
   332  	// Using `go list -m -f '{{ if .Main }}{{.GoMod}}{{ end }}' all` may try to
   333  	// generate a go.mod when a vendor tool is in use. To avoid that behavior
   334  	// we use `go env GOMOD` followed by an existence check.
   335  	cmd := exec.Command("go", "env", "GOMOD")
   336  	cmd.Env = append(os.Environ(), "GO111MODULE=on")
   337  	output, err := cmd.Output()
   338  	if err != nil {
   339  		return nil, wrapExitErr(err, "failed to determine module root")
   340  	}
   341  
   342  	modExists, err := regularFileExists(strings.TrimSpace(string(output)))
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  	if !modExists {
   347  		return nil, nil
   348  	}
   349  
   350  	return listModuleInfo()
   351  }
   352  
   353  type SourceDescriptor struct {
   354  	Name string
   355  	Path string
   356  }
   357  
   358  type Sources []SourceDescriptor
   359  
   360  func (s Sources) Len() int           { return len(s) }
   361  func (s Sources) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   362  func (s Sources) Less(i, j int) bool { return s[i].Name < s[j].Name }
   363  
   364  type SourceMap map[string]SourceDescriptor
   365  
   366  func (s SourceMap) Sources() Sources {
   367  	var sources Sources
   368  	for _, src := range s {
   369  		sources = append(sources, src)
   370  	}
   371  
   372  	sort.Sort(sources)
   373  	return sources
   374  }
   375  
   376  func (s SourceMap) Directories() []string {
   377  	dirMap := map[string]bool{}
   378  	for entryName := range s {
   379  		dir := path.Dir(entryName)
   380  		for dir != "." && !dirMap[dir] {
   381  			dirMap[dir] = true
   382  			dir = path.Dir(dir)
   383  		}
   384  	}
   385  
   386  	var dirs []string
   387  	for dir := range dirMap {
   388  		dirs = append(dirs, dir)
   389  	}
   390  	sort.Strings(dirs)
   391  
   392  	return dirs
   393  }
   394  
   395  func findSource(cd *CodeDescriptor) (SourceMap, error) {
   396  	sources := SourceMap{}
   397  
   398  	walkFn := func(path string, info os.FileInfo, err error) error {
   399  		if err != nil {
   400  			return err
   401  		}
   402  
   403  		if info.IsDir() {
   404  			// Allow import of the top level chaincode directory into chaincode code package
   405  			if path == cd.Source {
   406  				return nil
   407  			}
   408  
   409  			// Allow import of META-INF metadata directories into chaincode code package tar.
   410  			// META-INF directories contain chaincode metadata artifacts such as statedb index definitions
   411  			if cd.isMetadata(path) {
   412  				return nil
   413  			}
   414  
   415  			// include everything except hidden dirs when we're not vendoring
   416  			if cd.Module && !strings.HasPrefix(info.Name(), ".") {
   417  				return nil
   418  			}
   419  
   420  			// Do not import any other directories into chaincode code package
   421  			return filepath.SkipDir
   422  		}
   423  
   424  		relativeRoot := cd.Source
   425  		if cd.isMetadata(path) {
   426  			relativeRoot = cd.MetadataRoot
   427  		}
   428  
   429  		name, err := filepath.Rel(relativeRoot, path)
   430  		if err != nil {
   431  			return errors.Wrapf(err, "failed to calculate relative path for %s", path)
   432  		}
   433  
   434  		switch {
   435  		case cd.isMetadata(path):
   436  			// Skip hidden files in metadata
   437  			if strings.HasPrefix(info.Name(), ".") {
   438  				return nil
   439  			}
   440  			name = filepath.Join("META-INF", name)
   441  			err := validateMetadata(name, path)
   442  			if err != nil {
   443  				return err
   444  			}
   445  		case cd.Module:
   446  			name = filepath.Join("src", name)
   447  		default:
   448  			// skip top level go.mod and go.sum when not in module mode
   449  			if name == "go.mod" || name == "go.sum" {
   450  				return nil
   451  			}
   452  			name = filepath.Join("src", cd.Path, name)
   453  		}
   454  
   455  		name = filepath.ToSlash(name)
   456  		sources[name] = SourceDescriptor{Name: name, Path: path}
   457  		return nil
   458  	}
   459  
   460  	if err := filepath.Walk(cd.Source, walkFn); err != nil {
   461  		return nil, errors.Wrap(err, "walk failed")
   462  	}
   463  
   464  	return sources, nil
   465  }
   466  
   467  func validateMetadata(name, path string) error {
   468  	contents, err := ioutil.ReadFile(path)
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	// Validate metadata file for inclusion in tar
   474  	// Validation is based on the passed filename with path
   475  	err = ccmetadata.ValidateMetadataFile(filepath.ToSlash(name), contents)
   476  	if err != nil {
   477  		return err
   478  	}
   479  
   480  	return nil
   481  }
   482  
   483  // dist holds go "distribution" information. The full list of distributions can
   484  // be obtained with `go tool dist list.
   485  type dist struct{ goos, goarch string }
   486  
   487  // distributions returns the list of OS and ARCH combinations that we calcluate
   488  // deps for.
   489  func distributions() []dist {
   490  	// pre-populate linux architecutures
   491  	dists := map[dist]bool{
   492  		{goos: "linux", goarch: "amd64"}: true,
   493  		{goos: "linux", goarch: "s390x"}: true,
   494  	}
   495  
   496  	// add local OS and ARCH
   497  	dists[dist{goos: runtime.GOOS, goarch: runtime.GOARCH}] = true
   498  
   499  	var list []dist
   500  	for d := range dists {
   501  		list = append(list, d)
   502  	}
   503  
   504  	return list
   505  }