github.com/vijaypunugubati/fabric@v2.0.0-alpha.0.20200109185758-70466159f5b3+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  // GetDeploymentPayload creates a gzip compressed tape archive that contains the
   112  // required assets to build and run go chaincode.
   113  //
   114  // NOTE: this is only used at the _client_ side by the peer CLI.
   115  func (p *Platform) GetDeploymentPayload(codepath string) ([]byte, error) {
   116  	codeDescriptor, err := DescribeCode(codepath)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	fileMap, err := findSource(codeDescriptor)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	var dependencyPackageInfo []PackageInfo
   127  	if !codeDescriptor.Module {
   128  		for _, dist := range distributions() {
   129  			pi, err := gopathDependencyPackageInfo(dist.goos, dist.goarch, codeDescriptor.Path)
   130  			if err != nil {
   131  				return nil, err
   132  			}
   133  			dependencyPackageInfo = append(dependencyPackageInfo, pi...)
   134  		}
   135  	}
   136  
   137  	for _, pkg := range dependencyPackageInfo {
   138  		for _, filename := range pkg.Files() {
   139  			sd := SourceDescriptor{
   140  				Name: path.Join("src", pkg.ImportPath, filename),
   141  				Path: filepath.Join(pkg.Dir, filename),
   142  			}
   143  			fileMap[sd.Name] = sd
   144  		}
   145  	}
   146  
   147  	payload := bytes.NewBuffer(nil)
   148  	gw := gzip.NewWriter(payload)
   149  	tw := tar.NewWriter(gw)
   150  
   151  	// Create directories so they get sane ownership and permissions
   152  	for _, dirname := range fileMap.Directories() {
   153  		err := tw.WriteHeader(&tar.Header{
   154  			Typeflag: tar.TypeDir,
   155  			Name:     dirname + "/",
   156  			Mode:     c_ISDIR | 0755,
   157  			Uid:      500,
   158  			Gid:      500,
   159  		})
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  
   165  	for _, file := range fileMap.Sources() {
   166  		err = util.WriteFileToPackage(file.Path, file.Name, tw)
   167  		if err != nil {
   168  			return nil, fmt.Errorf("Error writing %s to tar: %s", file.Name, err)
   169  		}
   170  	}
   171  
   172  	err = tw.Close()
   173  	if err == nil {
   174  		err = gw.Close()
   175  	}
   176  	if err != nil {
   177  		return nil, errors.Wrapf(err, "failed to create tar for chaincode")
   178  	}
   179  
   180  	return payload.Bytes(), nil
   181  }
   182  
   183  func (p *Platform) GenerateDockerfile() (string, error) {
   184  	var buf []string
   185  	buf = append(buf, "FROM "+util.GetDockerImageFromConfig("chaincode.golang.runtime"))
   186  	buf = append(buf, "ADD binpackage.tar /usr/local/bin")
   187  
   188  	return strings.Join(buf, "\n"), nil
   189  }
   190  
   191  const staticLDFlagsOpts = "-ldflags \"-linkmode external -extldflags '-static'\""
   192  const dynamicLDFlagsOpts = ""
   193  
   194  func getLDFlagsOpts() string {
   195  	if viper.GetBool("chaincode.golang.dynamicLink") {
   196  		return dynamicLDFlagsOpts
   197  	}
   198  	return staticLDFlagsOpts
   199  }
   200  
   201  var buildScript = `
   202  set -e
   203  if [ -f "/chaincode/input/src/go.mod" ] && [ -d "/chaincode/input/src/vendor" ]; then
   204      cd /chaincode/input/src
   205      GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode %[2]s
   206  elif [ -f "/chaincode/input/src/go.mod" ]; then
   207      cd /chaincode/input/src
   208      GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode %[2]s
   209  elif [ -f "/chaincode/input/src/%[2]s/go.mod" ] && [ -d "/chaincode/input/src/%[2]s/vendor" ]; then
   210      cd /chaincode/input/src/%[2]s
   211      GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode .
   212  elif [ -f "/chaincode/input/src/%[2]s/go.mod" ]; then
   213      cd /chaincode/input/src/%[2]s
   214      GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode .
   215  else
   216      GOPATH=/chaincode/input:$GOPATH go build -v %[1]s -o /chaincode/output/chaincode %[2]s
   217  fi
   218  echo Done!
   219  `
   220  
   221  func (p *Platform) DockerBuildOptions(path string) (util.DockerBuildOptions, error) {
   222  	env := []string{}
   223  	for _, key := range []string{"GOPROXY", "GOSUMDB"} {
   224  		if val, ok := os.LookupEnv(key); ok {
   225  			env = append(env, fmt.Sprintf("%s=%s", key, val))
   226  			continue
   227  		}
   228  		if key == "GOPROXY" {
   229  			env = append(env, "GOPROXY=https://proxy.golang.org")
   230  		}
   231  	}
   232  	ldFlagOpts := getLDFlagsOpts()
   233  	return util.DockerBuildOptions{
   234  		Cmd: fmt.Sprintf(buildScript, ldFlagOpts, path),
   235  		Env: env,
   236  	}, nil
   237  }
   238  
   239  // CodeDescriptor describes the code we're packaging.
   240  type CodeDescriptor struct {
   241  	Source       string // absolute path of the source to package
   242  	MetadataRoot string // absolute path META-INF
   243  	Path         string // import path of the package
   244  	Module       bool   // does this represent a go module
   245  }
   246  
   247  func (cd CodeDescriptor) isMetadata(path string) bool {
   248  	return strings.HasPrefix(
   249  		filepath.Clean(path),
   250  		filepath.Clean(cd.MetadataRoot),
   251  	)
   252  }
   253  
   254  // DescribeCode returns GOPATH and package information.
   255  func DescribeCode(path string) (*CodeDescriptor, error) {
   256  	if path == "" {
   257  		return nil, errors.New("cannot collect files from empty chaincode path")
   258  	}
   259  
   260  	// Use the module root as the source path for go modules
   261  	modInfo, err := moduleInfo(path)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	if modInfo != nil {
   267  		// calculate where the metadata should be relative to module root
   268  		relImport, err := filepath.Rel(modInfo.ModulePath, modInfo.ImportPath)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  
   273  		return &CodeDescriptor{
   274  			Module:       true,
   275  			MetadataRoot: filepath.Join(modInfo.Dir, relImport, "META-INF"),
   276  			Path:         modInfo.ImportPath,
   277  			Source:       modInfo.Dir,
   278  		}, nil
   279  	}
   280  
   281  	return describeGopath(path)
   282  }
   283  
   284  func describeGopath(importPath string) (*CodeDescriptor, error) {
   285  	output, err := exec.Command("go", "list", "-f", "{{.Dir}}", importPath).Output()
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	sourcePath := filepath.Clean(strings.TrimSpace(string(output)))
   290  
   291  	return &CodeDescriptor{
   292  		Path:         importPath,
   293  		MetadataRoot: filepath.Join(sourcePath, "META-INF"),
   294  		Source:       sourcePath,
   295  	}, nil
   296  }
   297  
   298  func regularFileExists(path string) (bool, error) {
   299  	fi, err := os.Stat(path)
   300  	switch {
   301  	case os.IsNotExist(err):
   302  		return false, nil
   303  	case err != nil:
   304  		return false, err
   305  	default:
   306  		return fi.Mode().IsRegular(), nil
   307  	}
   308  }
   309  
   310  func moduleInfo(path string) (*ModuleInfo, error) {
   311  	entryWD, err := os.Getwd()
   312  	if err != nil {
   313  		return nil, errors.Wrap(err, "failed to get working directory")
   314  	}
   315  
   316  	// directory doesn't exist so unlikely to be a module
   317  	if err := os.Chdir(path); err != nil {
   318  		return nil, nil
   319  	}
   320  	defer func() {
   321  		if err := os.Chdir(entryWD); err != nil {
   322  			panic(fmt.Sprintf("failed to restore working directory: %s", err))
   323  		}
   324  	}()
   325  
   326  	// Using `go list -m -f '{{ if .Main }}{{.GoMod}}{{ end }}' all` may try to
   327  	// generate a go.mod when a vendor tool is in use. To avoid that behavior
   328  	// we use `go env GOMOD` followed by an existence check.
   329  	cmd := exec.Command("go", "env", "GOMOD")
   330  	cmd.Env = append(os.Environ(), "GO111MODULE=on")
   331  	output, err := cmd.Output()
   332  	if err != nil {
   333  		return nil, errors.Wrap(err, "failed to determine module root")
   334  	}
   335  
   336  	modExists, err := regularFileExists(strings.TrimSpace(string(output)))
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	if !modExists {
   341  		return nil, nil
   342  	}
   343  
   344  	return listModuleInfo()
   345  }
   346  
   347  type SourceDescriptor struct {
   348  	Name string
   349  	Path string
   350  }
   351  
   352  type Sources []SourceDescriptor
   353  
   354  func (s Sources) Len() int           { return len(s) }
   355  func (s Sources) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   356  func (s Sources) Less(i, j int) bool { return s[i].Name < s[j].Name }
   357  
   358  type SourceMap map[string]SourceDescriptor
   359  
   360  func (s SourceMap) Sources() Sources {
   361  	var sources Sources
   362  	for _, src := range s {
   363  		sources = append(sources, src)
   364  	}
   365  
   366  	sort.Sort(sources)
   367  	return sources
   368  }
   369  
   370  func (s SourceMap) Directories() []string {
   371  	dirMap := map[string]bool{}
   372  	for entryName := range s {
   373  		dir := path.Dir(entryName)
   374  		for dir != "." && !dirMap[dir] {
   375  			dirMap[dir] = true
   376  			dir = path.Dir(dir)
   377  		}
   378  	}
   379  
   380  	var dirs []string
   381  	for dir := range dirMap {
   382  		dirs = append(dirs, dir)
   383  	}
   384  	sort.Strings(dirs)
   385  
   386  	return dirs
   387  }
   388  
   389  func findSource(cd *CodeDescriptor) (SourceMap, error) {
   390  	sources := SourceMap{}
   391  
   392  	walkFn := func(path string, info os.FileInfo, err error) error {
   393  		if err != nil {
   394  			return err
   395  		}
   396  
   397  		if info.IsDir() {
   398  			// Allow import of the top level chaincode directory into chaincode code package
   399  			if path == cd.Source {
   400  				return nil
   401  			}
   402  
   403  			// Allow import of META-INF metadata directories into chaincode code package tar.
   404  			// META-INF directories contain chaincode metadata artifacts such as statedb index definitions
   405  			if cd.isMetadata(path) {
   406  				return nil
   407  			}
   408  
   409  			// include everything except hidden dirs when we're not vendoring
   410  			if cd.Module && !strings.HasPrefix(info.Name(), ".") {
   411  				return nil
   412  			}
   413  
   414  			// Do not import any other directories into chaincode code package
   415  			return filepath.SkipDir
   416  		}
   417  
   418  		relativeRoot := cd.Source
   419  		if cd.isMetadata(path) {
   420  			relativeRoot = cd.MetadataRoot
   421  		}
   422  
   423  		name, err := filepath.Rel(relativeRoot, path)
   424  		if err != nil {
   425  			return errors.Wrapf(err, "failed to calculate relative path for %s", path)
   426  		}
   427  
   428  		switch {
   429  		case cd.isMetadata(path):
   430  			// Skip hidden files in metadata
   431  			if strings.HasPrefix(info.Name(), ".") {
   432  				return nil
   433  			}
   434  			name = filepath.Join("META-INF", name)
   435  			err := validateMetadata(name, path)
   436  			if err != nil {
   437  				return err
   438  			}
   439  		case cd.Module:
   440  			name = filepath.Join("src", name)
   441  		default:
   442  			name = filepath.Join("src", cd.Path, name)
   443  		}
   444  
   445  		name = filepath.ToSlash(name)
   446  		sources[name] = SourceDescriptor{Name: name, Path: path}
   447  		return nil
   448  	}
   449  
   450  	if err := filepath.Walk(cd.Source, walkFn); err != nil {
   451  		return nil, errors.Wrap(err, "walk failed")
   452  	}
   453  
   454  	return sources, nil
   455  }
   456  
   457  func validateMetadata(name, path string) error {
   458  	contents, err := ioutil.ReadFile(path)
   459  	if err != nil {
   460  		return err
   461  	}
   462  
   463  	// Validate metadata file for inclusion in tar
   464  	// Validation is based on the passed filename with path
   465  	err = ccmetadata.ValidateMetadataFile(filepath.ToSlash(name), contents)
   466  	if err != nil {
   467  		return err
   468  	}
   469  
   470  	return nil
   471  }
   472  
   473  // dist holds go "distribution" information. The full list of distributions can
   474  // be obtained with `go tool dist list.
   475  type dist struct{ goos, goarch string }
   476  
   477  // distributions returns the list of OS and ARCH combinations that we calcluate
   478  // deps for.
   479  func distributions() []dist {
   480  	// pre-populate linux architecutures
   481  	dists := map[dist]bool{
   482  		{goos: "linux", goarch: "amd64"}: true,
   483  		{goos: "linux", goarch: "s390x"}: true,
   484  	}
   485  
   486  	// add local OS and ARCH
   487  	dists[dist{goos: runtime.GOOS, goarch: runtime.GOARCH}] = true
   488  
   489  	var list []dist
   490  	for d := range dists {
   491  		list = append(list, d)
   492  	}
   493  
   494  	return list
   495  }