github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/core/chaincode/platforms/golang/platform.go (about) 1 /* 2 Copyright hechain. 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 "github.com/hechain20/hechain/core/chaincode/platforms/util" 26 "github.com/hechain20/hechain/internal/ccmetadata" 27 pb "github.com/hyperledger/fabric-protos-go/peer" 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|0o777) != 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 = 0o40000 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 | 0o755, 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 ( 198 staticLDFlagsOpts = "-ldflags \"-linkmode external -extldflags '-static'\"" 199 dynamicLDFlagsOpts = "" 200 ) 201 202 func getLDFlagsOpts() string { 203 if viper.GetBool("chaincode.golang.dynamicLink") { 204 return dynamicLDFlagsOpts 205 } 206 return staticLDFlagsOpts 207 } 208 209 var buildScript = ` 210 set -e 211 if [ -f "/chaincode/input/src/go.mod" ] && [ -d "/chaincode/input/src/vendor" ]; then 212 cd /chaincode/input/src 213 GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode %[2]s 214 elif [ -f "/chaincode/input/src/go.mod" ]; then 215 cd /chaincode/input/src 216 GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode %[2]s 217 elif [ -f "/chaincode/input/src/%[2]s/go.mod" ] && [ -d "/chaincode/input/src/%[2]s/vendor" ]; then 218 cd /chaincode/input/src/%[2]s 219 GO111MODULE=on go build -v -mod=vendor %[1]s -o /chaincode/output/chaincode . 220 elif [ -f "/chaincode/input/src/%[2]s/go.mod" ]; then 221 cd /chaincode/input/src/%[2]s 222 GO111MODULE=on go build -v -mod=readonly %[1]s -o /chaincode/output/chaincode . 223 else 224 GO111MODULE=off GOPATH=/chaincode/input:$GOPATH go build -v %[1]s -o /chaincode/output/chaincode %[2]s 225 fi 226 echo Done! 227 ` 228 229 func (p *Platform) DockerBuildOptions(path string) (util.DockerBuildOptions, error) { 230 env := []string{} 231 for _, key := range []string{"GOPROXY", "GOSUMDB"} { 232 if val, ok := os.LookupEnv(key); ok { 233 env = append(env, fmt.Sprintf("%s=%s", key, val)) 234 continue 235 } 236 if key == "GOPROXY" { 237 env = append(env, "GOPROXY=https://proxy.golang.org") 238 } 239 } 240 ldFlagOpts := getLDFlagsOpts() 241 return util.DockerBuildOptions{ 242 Cmd: fmt.Sprintf(buildScript, ldFlagOpts, path), 243 Env: env, 244 }, nil 245 } 246 247 // CodeDescriptor describes the code we're packaging. 248 type CodeDescriptor struct { 249 Source string // absolute path of the source to package 250 MetadataRoot string // absolute path META-INF 251 Path string // import path of the package 252 Module bool // does this represent a go module 253 } 254 255 func (cd CodeDescriptor) isMetadata(path string) bool { 256 return strings.HasPrefix( 257 filepath.Clean(path), 258 filepath.Clean(cd.MetadataRoot), 259 ) 260 } 261 262 // DescribeCode returns GOPATH and package information. 263 func DescribeCode(path string) (*CodeDescriptor, error) { 264 if path == "" { 265 return nil, errors.New("cannot collect files from empty chaincode path") 266 } 267 268 // Use the module root as the source path for go modules 269 modInfo, err := moduleInfo(path) 270 if err != nil { 271 return nil, err 272 } 273 274 if modInfo != nil { 275 // calculate where the metadata should be relative to module root 276 relImport, err := filepath.Rel(modInfo.ModulePath, modInfo.ImportPath) 277 if err != nil { 278 return nil, err 279 } 280 281 return &CodeDescriptor{ 282 Module: true, 283 MetadataRoot: filepath.Join(modInfo.Dir, relImport, "META-INF"), 284 Path: modInfo.ImportPath, 285 Source: modInfo.Dir, 286 }, nil 287 } 288 289 return describeGopath(path) 290 } 291 292 func describeGopath(importPath string) (*CodeDescriptor, error) { 293 output, err := exec.Command("go", "list", "-f", "{{.Dir}}", importPath).Output() 294 if err != nil { 295 return nil, wrapExitErr(err, "'go list' failed") 296 } 297 sourcePath := filepath.Clean(strings.TrimSpace(string(output))) 298 299 return &CodeDescriptor{ 300 Path: importPath, 301 MetadataRoot: filepath.Join(sourcePath, "META-INF"), 302 Source: sourcePath, 303 }, nil 304 } 305 306 func regularFileExists(path string) (bool, error) { 307 fi, err := os.Stat(path) 308 switch { 309 case os.IsNotExist(err): 310 return false, nil 311 case err != nil: 312 return false, err 313 default: 314 return fi.Mode().IsRegular(), nil 315 } 316 } 317 318 func moduleInfo(path string) (*ModuleInfo, error) { 319 entryWD, err := os.Getwd() 320 if err != nil { 321 return nil, errors.Wrap(err, "failed to get working directory") 322 } 323 324 // directory doesn't exist so unlikely to be a module 325 if err := os.Chdir(path); err != nil { 326 return nil, nil 327 } 328 defer func() { 329 if err := os.Chdir(entryWD); err != nil { 330 panic(fmt.Sprintf("failed to restore working directory: %s", err)) 331 } 332 }() 333 334 // Using `go list -m -f '{{ if .Main }}{{.GoMod}}{{ end }}' all` may try to 335 // generate a go.mod when a vendor tool is in use. To avoid that behavior 336 // we use `go env GOMOD` followed by an existence check. 337 cmd := exec.Command("go", "env", "GOMOD") 338 cmd.Env = append(os.Environ(), "GO111MODULE=on") 339 output, err := cmd.Output() 340 if err != nil { 341 return nil, wrapExitErr(err, "failed to determine module root") 342 } 343 344 modExists, err := regularFileExists(strings.TrimSpace(string(output))) 345 if err != nil { 346 return nil, err 347 } 348 if !modExists { 349 return nil, nil 350 } 351 352 return listModuleInfo() 353 } 354 355 type SourceDescriptor struct { 356 Name string 357 Path string 358 } 359 360 type Sources []SourceDescriptor 361 362 func (s Sources) Len() int { return len(s) } 363 func (s Sources) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 364 func (s Sources) Less(i, j int) bool { return s[i].Name < s[j].Name } 365 366 type SourceMap map[string]SourceDescriptor 367 368 func (s SourceMap) Sources() Sources { 369 var sources Sources 370 for _, src := range s { 371 sources = append(sources, src) 372 } 373 374 sort.Sort(sources) 375 return sources 376 } 377 378 func (s SourceMap) Directories() []string { 379 dirMap := map[string]bool{} 380 for entryName := range s { 381 dir := path.Dir(entryName) 382 for dir != "." && !dirMap[dir] { 383 dirMap[dir] = true 384 dir = path.Dir(dir) 385 } 386 } 387 388 var dirs []string 389 for dir := range dirMap { 390 dirs = append(dirs, dir) 391 } 392 sort.Strings(dirs) 393 394 return dirs 395 } 396 397 func findSource(cd *CodeDescriptor) (SourceMap, error) { 398 sources := SourceMap{} 399 400 walkFn := func(path string, info os.FileInfo, err error) error { 401 if err != nil { 402 return err 403 } 404 405 if info.IsDir() { 406 // Allow import of the top level chaincode directory into chaincode code package 407 if path == cd.Source { 408 return nil 409 } 410 411 // Allow import of META-INF metadata directories into chaincode code package tar. 412 // META-INF directories contain chaincode metadata artifacts such as statedb index definitions 413 if cd.isMetadata(path) { 414 return nil 415 } 416 417 // include everything except hidden dirs when we're not vendoring 418 if cd.Module && !strings.HasPrefix(info.Name(), ".") { 419 return nil 420 } 421 422 // Do not import any other directories into chaincode code package 423 return filepath.SkipDir 424 } 425 426 relativeRoot := cd.Source 427 if cd.isMetadata(path) { 428 relativeRoot = cd.MetadataRoot 429 } 430 431 name, err := filepath.Rel(relativeRoot, path) 432 if err != nil { 433 return errors.Wrapf(err, "failed to calculate relative path for %s", path) 434 } 435 436 switch { 437 case cd.isMetadata(path): 438 // Skip hidden files in metadata 439 if strings.HasPrefix(info.Name(), ".") { 440 return nil 441 } 442 name = filepath.Join("META-INF", name) 443 err := validateMetadata(name, path) 444 if err != nil { 445 return err 446 } 447 case cd.Module: 448 name = filepath.Join("src", name) 449 default: 450 // skip top level go.mod and go.sum when not in module mode 451 if name == "go.mod" || name == "go.sum" { 452 return nil 453 } 454 name = filepath.Join("src", cd.Path, name) 455 } 456 457 name = filepath.ToSlash(name) 458 sources[name] = SourceDescriptor{Name: name, Path: path} 459 return nil 460 } 461 462 if err := filepath.Walk(cd.Source, walkFn); err != nil { 463 return nil, errors.Wrap(err, "walk failed") 464 } 465 466 return sources, nil 467 } 468 469 func validateMetadata(name, path string) error { 470 contents, err := ioutil.ReadFile(path) 471 if err != nil { 472 return err 473 } 474 475 // Validate metadata file for inclusion in tar 476 // Validation is based on the passed filename with path 477 err = ccmetadata.ValidateMetadataFile(filepath.ToSlash(name), contents) 478 if err != nil { 479 return err 480 } 481 482 return nil 483 } 484 485 // dist holds go "distribution" information. The full list of distributions can 486 // be obtained with `go tool dist list. 487 type dist struct{ goos, goarch string } 488 489 // distributions returns the list of OS and ARCH combinations that we calcluate 490 // deps for. 491 func distributions() []dist { 492 // pre-populate linux architecutures 493 dists := map[dist]bool{ 494 {goos: "linux", goarch: "amd64"}: true, 495 } 496 497 // add local OS and ARCH, linux and current ARCH 498 dists[dist{goos: runtime.GOOS, goarch: runtime.GOARCH}] = true 499 dists[dist{goos: "linux", goarch: runtime.GOARCH}] = true 500 501 var list []dist 502 for d := range dists { 503 list = append(list, d) 504 } 505 506 return list 507 }