github.com/yacovm/fabric@v2.0.0-alpha.0.20191128145320-c5d4087dc723+incompatible/core/container/externalbuilder/externalbuilder.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package externalbuilder 8 9 import ( 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "regexp" 18 "time" 19 20 "github.com/hyperledger/fabric/common/flogging" 21 "github.com/hyperledger/fabric/core/chaincode/persistence" 22 "github.com/hyperledger/fabric/core/container/ccintf" 23 "github.com/hyperledger/fabric/core/peer" 24 "github.com/pkg/errors" 25 ) 26 27 var ( 28 // DefaultEnvWhitelist enumerates the list of environment variables that are 29 // implicitly propagated to external builder and launcher commands. 30 DefaultEnvWhitelist = []string{"LD_LIBRARY_PATH", "LIBPATH", "PATH", "TMPDIR"} 31 32 logger = flogging.MustGetLogger("chaincode.externalbuilder") 33 ) 34 35 // BuildInfo contains metadata is that is saved to the local file system with the 36 // assets generated by an external builder. This is used to associate build output 37 // with the builder that generated it. 38 type BuildInfo struct { 39 // BuilderName is the user provided name of the external builder. 40 BuilderName string `json:"builder_name"` 41 } 42 43 // A Detector is responsible for orchestrating the external builder detection and 44 // build process. 45 type Detector struct { 46 // DurablePath is the file system location where chaincode assets are persisted. 47 DurablePath string 48 // Builders are the builders that detect and build processing will use. 49 Builders []*Builder 50 } 51 52 // CachedBuild returns a build instance that was already built or nil when no 53 // instance has been found. An error is returned only when an unexpected 54 // condition is encountered. 55 func (d *Detector) CachedBuild(ccid string) (*Instance, error) { 56 durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid)) 57 _, err := os.Stat(durablePath) 58 if os.IsNotExist(err) { 59 return nil, nil 60 } 61 if err != nil { 62 return nil, errors.WithMessage(err, "existing build detected, but something went wrong inspecting it") 63 } 64 65 buildInfoPath := filepath.Join(durablePath, "build-info.json") 66 buildInfoData, err := ioutil.ReadFile(buildInfoPath) 67 if err != nil { 68 return nil, errors.WithMessagef(err, "could not read '%s' for build info", buildInfoPath) 69 } 70 71 var buildInfo BuildInfo 72 if err := json.Unmarshal(buildInfoData, &buildInfo); err != nil { 73 return nil, errors.WithMessagef(err, "malformed build info at '%s'", buildInfoPath) 74 } 75 76 for _, builder := range d.Builders { 77 if builder.Name == buildInfo.BuilderName { 78 return &Instance{ 79 PackageID: ccid, 80 Builder: builder, 81 BldDir: filepath.Join(durablePath, "bld"), 82 ReleaseDir: filepath.Join(durablePath, "release"), 83 TermTimeout: 5 * time.Second, 84 }, nil 85 } 86 } 87 88 return nil, errors.Errorf("chaincode '%s' was already built with builder '%s', but that builder is no longer available", ccid, buildInfo.BuilderName) 89 } 90 91 // Build executes the external builder detect and build process. 92 // 93 // Before running the detect and build process, the detector first checks the 94 // durable path for the results of a previous build for the provided package. 95 // If found, the detect and build process is skipped and the existing instance 96 // is returned. 97 func (d *Detector) Build(ccid string, md *persistence.ChaincodePackageMetadata, codeStream io.Reader) (*Instance, error) { 98 // A small optimization: prevent exploding the build package out into the 99 // file system unless there are external builders defined. 100 if len(d.Builders) == 0 { 101 return nil, nil 102 } 103 104 // Look for a cached instance. 105 i, err := d.CachedBuild(ccid) 106 if err != nil { 107 return nil, errors.WithMessage(err, "existing build could not be restored") 108 } 109 if i != nil { 110 return i, nil 111 } 112 113 buildContext, err := NewBuildContext(ccid, md, codeStream) 114 if err != nil { 115 return nil, errors.WithMessage(err, "could not create build context") 116 } 117 defer buildContext.Cleanup() 118 119 builder := d.detect(buildContext) 120 if builder == nil { 121 logger.Debugf("no external builder detected for %s", ccid) 122 return nil, nil 123 } 124 125 if err := builder.Build(buildContext); err != nil { 126 return nil, errors.WithMessage(err, "external builder failed to build") 127 } 128 129 if err := builder.Release(buildContext); err != nil { 130 return nil, errors.WithMessage(err, "external builder failed to release") 131 } 132 133 durablePath := filepath.Join(d.DurablePath, SanitizeCCIDPath(ccid)) 134 135 err = os.Mkdir(durablePath, 0700) 136 if err != nil { 137 return nil, errors.WithMessagef(err, "could not create dir '%s' to persist build output", durablePath) 138 } 139 140 buildInfo, err := json.Marshal(&BuildInfo{ 141 BuilderName: builder.Name, 142 }) 143 if err != nil { 144 os.RemoveAll(durablePath) 145 return nil, errors.WithMessage(err, "could not marshal for build-info.json") 146 } 147 148 err = ioutil.WriteFile(filepath.Join(durablePath, "build-info.json"), buildInfo, 0600) 149 if err != nil { 150 os.RemoveAll(durablePath) 151 return nil, errors.WithMessage(err, "could not write build-info.json") 152 } 153 154 durableReleaseDir := filepath.Join(durablePath, "release") 155 err = os.Rename(buildContext.ReleaseDir, durableReleaseDir) 156 if err != nil { 157 os.RemoveAll(durablePath) 158 return nil, errors.WithMessagef(err, "could not move build context release to persistent location '%s'", durablePath) 159 } 160 161 durableBldDir := filepath.Join(durablePath, "bld") 162 err = os.Rename(buildContext.BldDir, durableBldDir) 163 if err != nil { 164 os.RemoveAll(durablePath) 165 return nil, errors.WithMessagef(err, "could not move build context bld to persistent location '%s'", durablePath) 166 } 167 168 return &Instance{ 169 PackageID: ccid, 170 Builder: builder, 171 BldDir: durableBldDir, 172 ReleaseDir: durableReleaseDir, 173 TermTimeout: 5 * time.Second, 174 }, nil 175 } 176 177 func (d *Detector) detect(buildContext *BuildContext) *Builder { 178 for _, builder := range d.Builders { 179 if builder.Detect(buildContext) { 180 return builder 181 } 182 } 183 return nil 184 } 185 186 // BuildContext holds references to the various assets locations necessary to 187 // execute the detect, build, release, and run programs for external builders 188 type BuildContext struct { 189 CCID string 190 Metadata *persistence.ChaincodePackageMetadata 191 ScratchDir string 192 SourceDir string 193 ReleaseDir string 194 MetadataDir string 195 BldDir string 196 } 197 198 // NewBuildContext creates the directories required to runt he external 199 // build process and extracts the chaincode package assets. 200 // 201 // Users of the BuildContext must call Cleanup when the build process is 202 // complete to remove the transient file system assets. 203 func NewBuildContext(ccid string, md *persistence.ChaincodePackageMetadata, codePackage io.Reader) (bc *BuildContext, err error) { 204 scratchDir, err := ioutil.TempDir("", "fabric-"+SanitizeCCIDPath(ccid)) 205 if err != nil { 206 return nil, errors.WithMessage(err, "could not create temp dir") 207 } 208 209 defer func() { 210 if err != nil { 211 os.RemoveAll(scratchDir) 212 } 213 }() 214 215 sourceDir := filepath.Join(scratchDir, "src") 216 if err = os.Mkdir(sourceDir, 0700); err != nil { 217 return nil, errors.WithMessage(err, "could not create source dir") 218 } 219 220 metadataDir := filepath.Join(scratchDir, "metadata") 221 if err = os.Mkdir(metadataDir, 0700); err != nil { 222 return nil, errors.WithMessage(err, "could not create metadata dir") 223 } 224 225 outputDir := filepath.Join(scratchDir, "bld") 226 if err = os.Mkdir(outputDir, 0700); err != nil { 227 return nil, errors.WithMessage(err, "could not create build dir") 228 } 229 230 releaseDir := filepath.Join(scratchDir, "release") 231 if err = os.Mkdir(releaseDir, 0700); err != nil { 232 return nil, errors.WithMessage(err, "could not create release dir") 233 } 234 235 err = Untar(codePackage, sourceDir) 236 if err != nil { 237 return nil, errors.WithMessage(err, "could not untar source package") 238 } 239 240 err = writeMetadataFile(ccid, md, metadataDir) 241 if err != nil { 242 return nil, errors.WithMessage(err, "could not write metadata file") 243 } 244 245 return &BuildContext{ 246 ScratchDir: scratchDir, 247 SourceDir: sourceDir, 248 MetadataDir: metadataDir, 249 BldDir: outputDir, 250 ReleaseDir: releaseDir, 251 Metadata: md, 252 CCID: ccid, 253 }, nil 254 } 255 256 // Cleanup removes the build context artifacts. 257 func (bc *BuildContext) Cleanup() { 258 os.RemoveAll(bc.ScratchDir) 259 } 260 261 var pkgIDreg = regexp.MustCompile("[<>:\"/\\\\|\\?\\*&]") 262 263 // SanitizeCCIDPath is used to ensure that special characters are removed from 264 // file names. 265 func SanitizeCCIDPath(ccid string) string { 266 return pkgIDreg.ReplaceAllString(ccid, "-") 267 } 268 269 type buildMetadata struct { 270 Path string `json:"path"` 271 Type string `json:"type"` 272 PackageID string `json:"package_id"` 273 } 274 275 func writeMetadataFile(ccid string, md *persistence.ChaincodePackageMetadata, dst string) error { 276 buildMetadata := &buildMetadata{ 277 Path: md.Path, 278 Type: md.Type, 279 PackageID: ccid, 280 } 281 mdBytes, err := json.Marshal(buildMetadata) 282 if err != nil { 283 return errors.Wrap(err, "failed to marshal build metadata into JSON") 284 } 285 286 return ioutil.WriteFile(filepath.Join(dst, "metadata.json"), mdBytes, 0700) 287 } 288 289 // A Builder is used to interact with an external chaincode builder and launcher. 290 type Builder struct { 291 EnvWhitelist []string 292 Location string 293 Logger *flogging.FabricLogger 294 Name string 295 } 296 297 // CreateBuilders will construct builders from the peer configuration. 298 func CreateBuilders(builderConfs []peer.ExternalBuilder) []*Builder { 299 var builders []*Builder 300 for _, builderConf := range builderConfs { 301 builders = append(builders, &Builder{ 302 Location: builderConf.Path, 303 Name: builderConf.Name, 304 EnvWhitelist: builderConf.EnvironmentWhitelist, 305 Logger: logger.Named(builderConf.Name), 306 }) 307 } 308 return builders 309 } 310 311 // Detect runs the `detect` script. 312 func (b *Builder) Detect(buildContext *BuildContext) bool { 313 detect := filepath.Join(b.Location, "bin", "detect") 314 cmd := b.NewCommand(detect, buildContext.SourceDir, buildContext.MetadataDir) 315 316 err := b.runCommand(cmd) 317 if err != nil { 318 logger.Debugf("builder '%s' detect failed: %s", b.Name, err) 319 return false 320 } 321 322 return true 323 } 324 325 // Build runs the `build` script. 326 func (b *Builder) Build(buildContext *BuildContext) error { 327 build := filepath.Join(b.Location, "bin", "build") 328 cmd := b.NewCommand(build, buildContext.SourceDir, buildContext.MetadataDir, buildContext.BldDir) 329 330 err := b.runCommand(cmd) 331 if err != nil { 332 return errors.Wrapf(err, "external builder '%s' failed", b.Name) 333 } 334 335 return nil 336 } 337 338 // Release runs the `release` script. 339 func (b *Builder) Release(buildContext *BuildContext) error { 340 release := filepath.Join(b.Location, "bin", "release") 341 342 _, err := os.Stat(release) 343 if os.IsNotExist(err) { 344 b.Logger.Debugf("Skipping release step for '%s' as no release binary found", buildContext.CCID) 345 return nil 346 } 347 if err != nil { 348 return errors.WithMessagef(err, "could not stat release binary '%s'", release) 349 } 350 351 cmd := b.NewCommand(release, buildContext.BldDir, buildContext.ReleaseDir) 352 err = b.runCommand(cmd) 353 if err != nil { 354 return errors.Wrapf(err, "builder '%s' release failed", b.Name) 355 } 356 357 return nil 358 } 359 360 // runConfig is serialized to disk when launching. 361 type runConfig struct { 362 CCID string `json:"chaincode_id"` 363 PeerAddress string `json:"peer_address"` 364 ClientCert string `json:"client_cert"` // PEM encoded client certificate 365 ClientKey string `json:"client_key"` // PEM encoded client key 366 RootCert string `json:"root_cert"` // PEM encoded peer chaincode certificate 367 } 368 369 func newRunConfig(ccid string, peerConnection *ccintf.PeerConnection) runConfig { 370 var tlsConfig ccintf.TLSConfig 371 if peerConnection.TLSConfig != nil { 372 tlsConfig = *peerConnection.TLSConfig 373 } 374 375 return runConfig{ 376 PeerAddress: peerConnection.Address, 377 CCID: ccid, 378 ClientCert: string(tlsConfig.ClientCert), 379 ClientKey: string(tlsConfig.ClientKey), 380 RootCert: string(tlsConfig.RootCert), 381 } 382 } 383 384 // Run starts the `run` script and returns a Session that can be used to 385 // signal it and wait for termination. 386 func (b *Builder) Run(ccid, bldDir string, peerConnection *ccintf.PeerConnection) (*Session, error) { 387 launchDir, err := ioutil.TempDir("", "fabric-run") 388 if err != nil { 389 return nil, errors.WithMessage(err, "could not create temp run dir") 390 } 391 392 rc := newRunConfig(ccid, peerConnection) 393 marshaledRC, err := json.Marshal(rc) 394 if err != nil { 395 return nil, errors.WithMessage(err, "could not marshal run config") 396 } 397 398 if err := ioutil.WriteFile(filepath.Join(launchDir, "chaincode.json"), marshaledRC, 0600); err != nil { 399 return nil, errors.WithMessage(err, "could not write root cert") 400 } 401 402 run := filepath.Join(b.Location, "bin", "run") 403 cmd := b.NewCommand(run, bldDir, launchDir) 404 sess, err := Start(b.Logger, cmd) 405 if err != nil { 406 os.RemoveAll(launchDir) 407 return nil, errors.Wrapf(err, "builder '%s' run failed to start", b.Name) 408 } 409 410 go func() { 411 defer os.RemoveAll(launchDir) 412 sess.Wait() 413 }() 414 415 return sess, nil 416 } 417 418 // runCommand runs a command and waits for it to complete. 419 func (b *Builder) runCommand(cmd *exec.Cmd) error { 420 sess, err := Start(b.Logger, cmd) 421 if err != nil { 422 return err 423 } 424 return sess.Wait() 425 } 426 427 // NewCommand creates an exec.Cmd that is configured to prune the calling 428 // environment down to the environment variables specified in the external 429 // builder's EnvironmentWhitelist and the DefaultEnvWhitelist. 430 func (b *Builder) NewCommand(name string, args ...string) *exec.Cmd { 431 cmd := exec.Command(name, args...) 432 whitelist := appendDefaultWhitelist(b.EnvWhitelist) 433 for _, key := range whitelist { 434 if val, ok := os.LookupEnv(key); ok { 435 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val)) 436 } 437 } 438 return cmd 439 } 440 441 func appendDefaultWhitelist(envWhitelist []string) []string { 442 for _, variable := range DefaultEnvWhitelist { 443 if !contains(envWhitelist, variable) { 444 envWhitelist = append(envWhitelist, variable) 445 } 446 } 447 return envWhitelist 448 } 449 450 func contains(envWhiteList []string, key string) bool { 451 for _, variable := range envWhiteList { 452 if key == variable { 453 return true 454 } 455 } 456 return false 457 }