github.com/hyperledger-labs/bdls@v2.1.1+incompatible/core/chaincode/platforms/golang/platform_test.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/filepath"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  
    23  	pb "github.com/hyperledger/fabric-protos-go/peer"
    24  	"github.com/hyperledger/fabric/core/chaincode/platforms/util"
    25  	"github.com/hyperledger/fabric/core/config/configtest"
    26  	"github.com/pkg/errors"
    27  	"github.com/spf13/viper"
    28  	"github.com/stretchr/testify/assert"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  func init() {
    33  	// Significantly reduce execution time of deployment payload tests.
    34  	gzipCompressionLevel = gzip.NoCompression
    35  }
    36  
    37  func generateFakeCDS(ccname, path, file string, mode int64) (*pb.ChaincodeDeploymentSpec, error) {
    38  	codePackage := bytes.NewBuffer(nil)
    39  	gw := gzip.NewWriter(codePackage)
    40  	tw := tar.NewWriter(gw)
    41  
    42  	payload := make([]byte, 25, 25)
    43  	err := tw.WriteHeader(&tar.Header{Name: file, Mode: mode, Size: int64(len(payload))})
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	if !strings.HasSuffix(file, "/") {
    48  		_, err = tw.Write(payload)
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  	}
    53  
    54  	tw.Close()
    55  	gw.Close()
    56  
    57  	cds := &pb.ChaincodeDeploymentSpec{
    58  		ChaincodeSpec: &pb.ChaincodeSpec{
    59  			ChaincodeId: &pb.ChaincodeID{
    60  				Name: ccname,
    61  				Path: path,
    62  			},
    63  		},
    64  		CodePackage: codePackage.Bytes(),
    65  	}
    66  
    67  	return cds, nil
    68  }
    69  
    70  func TestName(t *testing.T) {
    71  	platform := &Platform{}
    72  	assert.Equal(t, "GOLANG", platform.Name())
    73  }
    74  
    75  func TestValidatePath(t *testing.T) {
    76  	var tests = []struct {
    77  		path string
    78  		succ bool
    79  	}{
    80  		{path: "http://github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/chaincodes/noop", succ: false},
    81  		{path: "https://github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/chaincodes/noop", succ: false},
    82  		{path: "github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/chaincodes/noop", succ: true},
    83  		{path: "github.com/hyperledger/fabric/bad/chaincode/golang/testdata/src/chaincodes/noop", succ: false},
    84  		{path: ":github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/chaincodes/noop", succ: false},
    85  	}
    86  
    87  	for _, tt := range tests {
    88  		platform := &Platform{}
    89  		err := platform.ValidatePath(tt.path)
    90  		if tt.succ {
    91  			assert.NoError(t, err, "expected %s to be a valid path", tt.path)
    92  		} else {
    93  			assert.Errorf(t, err, "expected %s to be an invalid path", tt.path)
    94  		}
    95  	}
    96  }
    97  
    98  func TestNormalizePath(t *testing.T) {
    99  	tempdir, err := ioutil.TempDir("", "normalize-path")
   100  	require.NoError(t, err, "failed to create temporary directory")
   101  	defer os.RemoveAll(tempdir)
   102  
   103  	var tests = []struct {
   104  		path   string
   105  		result string
   106  	}{
   107  		{path: "github.com/hyperledger/fabric/cmd/peer", result: "github.com/hyperledger/fabric/cmd/peer"},
   108  		{path: "testdata/ccmodule", result: "ccmodule"},
   109  		{path: "missing", result: "missing"},
   110  		{path: tempdir, result: tempdir}, // /dev/null is returned from `go env GOMOD`
   111  	}
   112  	for i, tt := range tests {
   113  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   114  			platform := &Platform{}
   115  			result, err := platform.NormalizePath(tt.path)
   116  			assert.NoError(t, err, "normalize path failed")
   117  			assert.Equalf(t, tt.result, result, "want result %s got %s", tt.result, result)
   118  		})
   119  	}
   120  }
   121  
   122  // copied from the tar package
   123  const (
   124  	c_ISUID = 04000   // Set uid
   125  	c_ISGID = 02000   // Set gid
   126  	c_ISREG = 0100000 // Regular file
   127  )
   128  
   129  func TestValidateCodePackage(t *testing.T) {
   130  	tests := []struct {
   131  		name            string
   132  		path            string
   133  		file            string
   134  		mode            int64
   135  		successExpected bool
   136  	}{
   137  		{name: "NoCode", path: "path/to/somewhere", file: "/src/path/to/somewhere/main.go", mode: c_ISREG | 0400, successExpected: false},
   138  		{name: "NoCode", path: "path/to/somewhere", file: "src/path/to/somewhere/main.go", mode: c_ISREG | 0400, successExpected: true},
   139  		{name: "NoCode", path: "path/to/somewhere", file: "src/path/to/somewhere/main.go", mode: c_ISREG | 0644, successExpected: true},
   140  		{name: "NoCode", path: "path/to/somewhere", file: "src/path/to/somewhere/main.go", mode: c_ISREG | 0755, successExpected: true},
   141  		{name: "NoCode", path: "path/to/directory/", file: "src/path/to/directory/", mode: c_ISDIR | 0755, successExpected: true},
   142  		{name: "NoCode", path: "path/to/directory", file: "src/path/to/directory", mode: c_ISDIR | 0755, successExpected: true},
   143  		{name: "NoCode", path: "path/to/setuid", file: "src/path/to/setuid", mode: c_ISUID | 0755, successExpected: false},
   144  		{name: "NoCode", path: "path/to/setgid", file: "src/path/to/setgid", mode: c_ISGID | 0755, successExpected: false},
   145  		{name: "NoCode", path: "path/to/sticky/", file: "src/path/to/sticky/", mode: c_ISDIR | c_ISGID | 0755, successExpected: false},
   146  		{name: "NoCode", path: "path/to/somewhere", file: "META-INF/path/to/a/meta3", mode: 0100400, successExpected: true},
   147  	}
   148  
   149  	for _, tt := range tests {
   150  		cds, err := generateFakeCDS(tt.name, tt.path, tt.file, tt.mode)
   151  		assert.NoError(t, err, "failed to generate fake cds")
   152  
   153  		platform := &Platform{}
   154  		err = platform.ValidateCodePackage(cds.CodePackage)
   155  		if tt.successExpected {
   156  			assert.NoError(t, err, "expected success for path: %s, file: %s", tt.path, tt.file)
   157  		} else {
   158  			assert.Errorf(t, err, "expected error for path: %s, file: %s", tt.path, tt.file)
   159  		}
   160  	}
   161  }
   162  
   163  func getGopath() (string, error) {
   164  	output, err := exec.Command("go", "env", "GOPATH").Output()
   165  	if err != nil {
   166  		return "", err
   167  	}
   168  
   169  	pathElements := filepath.SplitList(strings.TrimSpace(string(output)))
   170  	if len(pathElements) == 0 {
   171  		return "", fmt.Errorf("GOPATH is not set")
   172  	}
   173  
   174  	return pathElements[0], nil
   175  }
   176  
   177  func Test_findSource(t *testing.T) {
   178  	t.Run("Gopath", func(t *testing.T) {
   179  		source, err := findSource(&CodeDescriptor{
   180  			Module:       false,
   181  			Source:       filepath.FromSlash("testdata/src/chaincodes/noop"),
   182  			MetadataRoot: filepath.FromSlash("testdata/src/chaincodes/noop/META-INF"),
   183  			Path:         "chaincodes/noop",
   184  		})
   185  		require.NoError(t, err, "failed to find source")
   186  		assert.Contains(t, source, "src/chaincodes/noop/chaincode.go")
   187  		assert.Contains(t, source, "META-INF/statedb/couchdb/indexes/indexOwner.json")
   188  		assert.NotContains(t, source, "src/chaincodes/noop/go.mod")
   189  		assert.NotContains(t, source, "src/chaincodes/noop/go.sum")
   190  		assert.Len(t, source, 2)
   191  	})
   192  
   193  	t.Run("Module", func(t *testing.T) {
   194  		source, err := findSource(&CodeDescriptor{
   195  			Module:       true,
   196  			Source:       filepath.FromSlash("testdata/ccmodule"),
   197  			MetadataRoot: filepath.FromSlash("testdata/ccmodule/META-INF"),
   198  			Path:         "ccmodule",
   199  		})
   200  		require.NoError(t, err, "failed to find source")
   201  		assert.Len(t, source, 7)
   202  		assert.Contains(t, source, "META-INF/statedb/couchdb/indexes/indexOwner.json")
   203  		assert.Contains(t, source, "src/go.mod")
   204  		assert.Contains(t, source, "src/go.sum")
   205  		assert.Contains(t, source, "src/chaincode.go")
   206  		assert.Contains(t, source, "src/customlogger/customlogger.go")
   207  		assert.Contains(t, source, "src/nested/chaincode.go")
   208  		assert.Contains(t, source, "src/nested/META-INF/statedb/couchdb/indexes/nestedIndexOwner.json")
   209  	})
   210  
   211  	t.Run("NonExistent", func(t *testing.T) {
   212  		_, err := findSource(&CodeDescriptor{Path: "acme.com/this/should/not/exist"})
   213  		assert.Error(t, err)
   214  		assert.True(t, os.IsNotExist(errors.Cause(err)))
   215  	})
   216  }
   217  
   218  func tarContents(t *testing.T, payload []byte) []string {
   219  	var files []string
   220  
   221  	is := bytes.NewReader(payload)
   222  	gr, err := gzip.NewReader(is)
   223  	require.NoError(t, err, "failed to create new gzip reader")
   224  
   225  	tr := tar.NewReader(gr)
   226  	for {
   227  		header, err := tr.Next()
   228  		if err == io.EOF {
   229  			break
   230  		}
   231  		assert.NoError(t, err)
   232  
   233  		if header.Typeflag == tar.TypeReg {
   234  			files = append(files, header.Name)
   235  		}
   236  	}
   237  
   238  	return files
   239  }
   240  
   241  func TestGopathDeploymentPayload(t *testing.T) {
   242  	platform := &Platform{}
   243  
   244  	payload, err := platform.GetDeploymentPayload("github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/chaincodes/noop")
   245  	assert.NoError(t, err)
   246  
   247  	contents := tarContents(t, payload)
   248  	assert.Contains(t, contents, "META-INF/statedb/couchdb/indexes/indexOwner.json")
   249  }
   250  
   251  func TestModuleDeploymentPayload(t *testing.T) {
   252  	platform := &Platform{}
   253  
   254  	t.Run("TopLevel", func(t *testing.T) {
   255  		dp, err := platform.GetDeploymentPayload("testdata/ccmodule")
   256  		assert.NoError(t, err)
   257  		contents := tarContents(t, dp)
   258  		assert.ElementsMatch(t, contents, []string{
   259  			"META-INF/statedb/couchdb/indexes/indexOwner.json", // top level metadata
   260  			"src/chaincode.go",
   261  			"src/customlogger/customlogger.go",
   262  			"src/go.mod",
   263  			"src/go.sum",
   264  			"src/nested/META-INF/statedb/couchdb/indexes/nestedIndexOwner.json",
   265  			"src/nested/chaincode.go",
   266  		})
   267  	})
   268  
   269  	t.Run("NestedPackage", func(t *testing.T) {
   270  		dp, err := platform.GetDeploymentPayload("testdata/ccmodule/nested")
   271  		assert.NoError(t, err)
   272  		contents := tarContents(t, dp)
   273  		assert.ElementsMatch(t, contents, []string{
   274  			"META-INF/statedb/couchdb/indexes/nestedIndexOwner.json", // nested metadata
   275  			"src/META-INF/statedb/couchdb/indexes/indexOwner.json",
   276  			"src/chaincode.go",
   277  			"src/customlogger/customlogger.go",
   278  			"src/go.mod",
   279  			"src/go.sum",
   280  			"src/nested/chaincode.go",
   281  		})
   282  	})
   283  }
   284  
   285  func updateGopath(t *testing.T, path string) func() {
   286  	initialGopath, set := os.LookupEnv("GOPATH")
   287  
   288  	if path == "" {
   289  		err := os.Unsetenv("GOPATH")
   290  		require.NoError(t, err)
   291  	} else {
   292  		err := os.Setenv("GOPATH", path)
   293  		require.NoError(t, err)
   294  	}
   295  
   296  	if !set {
   297  		return func() { os.Unsetenv("GOPATH") }
   298  	}
   299  	return func() { os.Setenv("GOPATH", initialGopath) }
   300  }
   301  
   302  func TestGetDeploymentPayload(t *testing.T) {
   303  	const testdata = "github.com/hyperledger/fabric/core/chaincode/platforms/golang/testdata/src/"
   304  
   305  	gopath, err := getGopath()
   306  	require.NoError(t, err)
   307  
   308  	platform := &Platform{}
   309  
   310  	var tests = []struct {
   311  		gopath string
   312  		path   string
   313  		succ   bool
   314  	}{
   315  		{gopath: gopath, path: testdata + "chaincodes/noop", succ: true},
   316  		{gopath: gopath, path: testdata + "bad/chaincodes/noop", succ: false},
   317  		{gopath: gopath, path: testdata + "chaincodes/BadImport", succ: false},
   318  		{gopath: gopath, path: testdata + "chaincodes/BadMetadataInvalidIndex", succ: false},
   319  		{gopath: gopath, path: testdata + "chaincodes/BadMetadataUnexpectedFolderContent", succ: false},
   320  		{gopath: gopath, path: testdata + "chaincodes/BadMetadataIgnoreHiddenFile", succ: true},
   321  		{gopath: gopath, path: testdata + "chaincodes/empty/", succ: false},
   322  		{gopath: "", path: "testdata/ccmodule", succ: true},
   323  	}
   324  
   325  	for i, tst := range tests {
   326  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   327  			reset := updateGopath(t, tst.gopath)
   328  			_, err := platform.GetDeploymentPayload(tst.path)
   329  			if tst.succ {
   330  				assert.NoError(t, err, "expected success for path: %s", tst.path)
   331  			} else {
   332  				assert.Errorf(t, err, "expected error for path: %s", tst.path)
   333  			}
   334  			reset()
   335  		})
   336  	}
   337  }
   338  
   339  func TestGenerateDockerFile(t *testing.T) {
   340  	platform := &Platform{}
   341  
   342  	viper.Set("chaincode.golang.runtime", "buildimage")
   343  	expected := "FROM buildimage\nADD binpackage.tar /usr/local/bin"
   344  	dockerfile, err := platform.GenerateDockerfile()
   345  	assert.NoError(t, err)
   346  	assert.Equal(t, expected, dockerfile)
   347  
   348  	viper.Set("chaincode.golang.runtime", "another-buildimage")
   349  	expected = "FROM another-buildimage\nADD binpackage.tar /usr/local/bin"
   350  	dockerfile, err = platform.GenerateDockerfile()
   351  	assert.NoError(t, err)
   352  	assert.Equal(t, expected, dockerfile)
   353  }
   354  
   355  func TestGetLDFlagsOpts(t *testing.T) {
   356  	viper.Set("chaincode.golang.dynamicLink", true)
   357  	if getLDFlagsOpts() != dynamicLDFlagsOpts {
   358  		t.Error("Error handling chaincode.golang.dynamicLink configuration. ldflags should be for dynamic linkink")
   359  	}
   360  	viper.Set("chaincode.golang.dynamicLink", false)
   361  	if getLDFlagsOpts() != staticLDFlagsOpts {
   362  		t.Error("Error handling chaincode.golang.dynamicLink configuration. ldflags should be for static linkink")
   363  	}
   364  }
   365  
   366  func TestDockerBuildOptions(t *testing.T) {
   367  	platform := &Platform{}
   368  
   369  	t.Run("GOPROXY and GOSUMDB not set", func(t *testing.T) {
   370  		opts, err := platform.DockerBuildOptions("the-path")
   371  		assert.NoError(t, err, "unexpected error from DockerBuildOptions")
   372  
   373  		expectedOpts := util.DockerBuildOptions{
   374  			Cmd: `
   375  set -e
   376  if [ -f "/chaincode/input/src/go.mod" ] && [ -d "/chaincode/input/src/vendor" ]; then
   377      cd /chaincode/input/src
   378      GO111MODULE=on go build -v -mod=vendor -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   379  elif [ -f "/chaincode/input/src/go.mod" ]; then
   380      cd /chaincode/input/src
   381      GO111MODULE=on go build -v -mod=readonly -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   382  elif [ -f "/chaincode/input/src/the-path/go.mod" ] && [ -d "/chaincode/input/src/the-path/vendor" ]; then
   383      cd /chaincode/input/src/the-path
   384      GO111MODULE=on go build -v -mod=vendor -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode .
   385  elif [ -f "/chaincode/input/src/the-path/go.mod" ]; then
   386      cd /chaincode/input/src/the-path
   387      GO111MODULE=on go build -v -mod=readonly -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode .
   388  else
   389      GOPATH=/chaincode/input:$GOPATH go build -v -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   390  fi
   391  echo Done!
   392  `,
   393  			Env: []string{"GOPROXY=https://proxy.golang.org"},
   394  		}
   395  		assert.Equal(t, expectedOpts, opts)
   396  	})
   397  
   398  	t.Run("GOPROXY and GOSUMDB set", func(t *testing.T) {
   399  		oldGoproxy, set := os.LookupEnv("GOPROXY")
   400  		if set {
   401  			defer os.Setenv("GOPROXY", oldGoproxy)
   402  		}
   403  		os.Setenv("GOPROXY", "the-goproxy")
   404  
   405  		oldGosumdb, set := os.LookupEnv("GOSUMDB")
   406  		if set {
   407  			defer os.Setenv("GOSUMDB", oldGosumdb)
   408  		}
   409  		os.Setenv("GOSUMDB", "the-gosumdb")
   410  
   411  		opts, err := platform.DockerBuildOptions("the-path")
   412  		assert.NoError(t, err, "unexpected error from DockerBuildOptions")
   413  
   414  		expectedOpts := util.DockerBuildOptions{
   415  			Cmd: `
   416  set -e
   417  if [ -f "/chaincode/input/src/go.mod" ] && [ -d "/chaincode/input/src/vendor" ]; then
   418      cd /chaincode/input/src
   419      GO111MODULE=on go build -v -mod=vendor -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   420  elif [ -f "/chaincode/input/src/go.mod" ]; then
   421      cd /chaincode/input/src
   422      GO111MODULE=on go build -v -mod=readonly -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   423  elif [ -f "/chaincode/input/src/the-path/go.mod" ] && [ -d "/chaincode/input/src/the-path/vendor" ]; then
   424      cd /chaincode/input/src/the-path
   425      GO111MODULE=on go build -v -mod=vendor -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode .
   426  elif [ -f "/chaincode/input/src/the-path/go.mod" ]; then
   427      cd /chaincode/input/src/the-path
   428      GO111MODULE=on go build -v -mod=readonly -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode .
   429  else
   430      GOPATH=/chaincode/input:$GOPATH go build -v -ldflags "-linkmode external -extldflags '-static'" -o /chaincode/output/chaincode the-path
   431  fi
   432  echo Done!
   433  `,
   434  			Env: []string{"GOPROXY=the-goproxy", "GOSUMDB=the-gosumdb"},
   435  		}
   436  		assert.Equal(t, expectedOpts, opts)
   437  	})
   438  }
   439  
   440  func TestDescribeCode(t *testing.T) {
   441  	abs, err := filepath.Abs(filepath.FromSlash("testdata/ccmodule"))
   442  	assert.NoError(t, err)
   443  
   444  	t.Run("TopLevelModulePackage", func(t *testing.T) {
   445  		cd, err := DescribeCode("testdata/ccmodule")
   446  		assert.NoError(t, err)
   447  		expected := &CodeDescriptor{
   448  			Source:       abs,
   449  			MetadataRoot: filepath.Join(abs, "META-INF"),
   450  			Path:         "ccmodule",
   451  			Module:       true,
   452  		}
   453  		assert.Equal(t, expected, cd)
   454  	})
   455  
   456  	t.Run("NestedModulePackage", func(t *testing.T) {
   457  		cd, err := DescribeCode("testdata/ccmodule/nested")
   458  		assert.NoError(t, err)
   459  		expected := &CodeDescriptor{
   460  			Source:       abs,
   461  			MetadataRoot: filepath.Join(abs, "nested", "META-INF"),
   462  			Path:         "ccmodule/nested",
   463  			Module:       true,
   464  		}
   465  		assert.Equal(t, expected, cd)
   466  	})
   467  }
   468  
   469  func TestRegularFileExists(t *testing.T) {
   470  	t.Run("RegularFile", func(t *testing.T) {
   471  		ok, err := regularFileExists("testdata/ccmodule/go.mod")
   472  		assert.NoError(t, err)
   473  		assert.True(t, ok)
   474  	})
   475  	t.Run("MissingFile", func(t *testing.T) {
   476  		ok, err := regularFileExists("testdata/missing.file")
   477  		assert.NoError(t, err)
   478  		assert.False(t, ok)
   479  	})
   480  	t.Run("Directory", func(t *testing.T) {
   481  		ok, err := regularFileExists("testdata")
   482  		assert.NoError(t, err)
   483  		assert.False(t, ok)
   484  	})
   485  }
   486  
   487  func TestMain(m *testing.M) {
   488  	viper.SetConfigName("core")
   489  	viper.SetEnvPrefix("CORE")
   490  	configtest.AddDevConfigPath(nil)
   491  	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
   492  	viper.AutomaticEnv()
   493  	if err := viper.ReadInConfig(); err != nil {
   494  		fmt.Printf("could not read config %s\n", err)
   495  		os.Exit(-1)
   496  	}
   497  	os.Exit(m.Run())
   498  }