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