github.com/Hnampk/fabric@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 }