github.com/yacovm/fabric@v2.0.0-alpha.0.20191128145320-c5d4087dc723+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 filename := range s { 373 dir := filepath.Dir(filename) 374 for dir != "." && !dirMap[dir] { 375 dirMap[dir] = true 376 dir = filepath.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 sources[name] = SourceDescriptor{Name: name, Path: path} 446 return nil 447 } 448 449 if err := filepath.Walk(cd.Source, walkFn); err != nil { 450 return nil, errors.Wrap(err, "walk failed") 451 } 452 453 return sources, nil 454 } 455 456 func validateMetadata(name, path string) error { 457 contents, err := ioutil.ReadFile(path) 458 if err != nil { 459 return err 460 } 461 462 // Validate metadata file for inclusion in tar 463 // Validation is based on the passed filename with path 464 err = ccmetadata.ValidateMetadataFile(name, contents) 465 if err != nil { 466 return err 467 } 468 469 return nil 470 } 471 472 // dist holds go "distribution" information. The full list of distributions can 473 // be obtained with `go tool dist list. 474 type dist struct{ goos, goarch string } 475 476 // distributions returns the list of OS and ARCH combinations that we calcluate 477 // deps for. 478 func distributions() []dist { 479 // pre-populate linux architecutures 480 dists := map[dist]bool{ 481 {goos: "linux", goarch: "amd64"}: true, 482 {goos: "linux", goarch: "s390x"}: true, 483 } 484 485 // add local OS and ARCH 486 dists[dist{goos: runtime.GOOS, goarch: runtime.GOARCH}] = true 487 488 var list []dist 489 for d := range dists { 490 list = append(list, d) 491 } 492 493 return list 494 }