github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/appfile/compile.go (about) 1 package appfile 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "sync" 14 15 "github.com/hashicorp/go-getter" 16 "github.com/hashicorp/go-multierror" 17 "github.com/hashicorp/otto/helper/oneline" 18 "github.com/hashicorp/terraform/dag" 19 ) 20 21 const ( 22 // CompileVersion is the current version that we're on for 23 // compilation formats. This can be used in the future to change 24 // the directory structure and on-disk format of compiled appfiles. 25 CompileVersion = 1 26 27 CompileFilename = "Appfile.compiled" 28 CompileDepsFolder = "deps" 29 CompileImportsFolder = "deps" 30 CompileVersionFilename = "version" 31 ) 32 33 // Compiled represents a "Compiled" Appfile. A compiled Appfile is one 34 // that has loaded all of its dependency Appfiles, completed its imports, 35 // verified it is valid, etc. 36 // 37 // Appfile compilation is a process that requires network activity and 38 // has to occur once. The idea is that after compilation, a fully compiled 39 // Appfile can then be loaded in the future without network connectivity. 40 // Additionally, since we can assume it is valid, we can load it very quickly. 41 type Compiled struct { 42 // File is the raw Appfile 43 File *File 44 45 // Graph is the DAG that has all the dependencies. This is already 46 // verified to have no cycles. Each vertex is a *CompiledGraphVertex. 47 Graph *dag.AcyclicGraph 48 } 49 50 func (c *Compiled) Validate() error { 51 var result error 52 53 // First validate that there are no cycles in the dependency graph 54 if cycles := c.Graph.Cycles(); len(cycles) > 0 { 55 for _, cycle := range cycles { 56 vertices := make([]string, len(cycle)) 57 for i, v := range cycle { 58 vertices[i] = dag.VertexName(v) 59 } 60 61 result = multierror.Append(result, fmt.Errorf( 62 "Dependency cycle: %s", strings.Join(vertices, ", "))) 63 } 64 } 65 66 // Validate all the files 67 var errLock sync.Mutex 68 c.Graph.Walk(func(raw dag.Vertex) error { 69 v := raw.(*CompiledGraphVertex) 70 if err := v.File.Validate(); err != nil { 71 errLock.Lock() 72 defer errLock.Unlock() 73 74 if s := v.File.Source; s != "" { 75 err = multierror.Prefix(err, fmt.Sprintf("Dependency %s:", s)) 76 } 77 78 result = multierror.Append(result, err) 79 } 80 81 return nil 82 }) 83 84 return result 85 } 86 87 func (c *Compiled) String() string { 88 var buf bytes.Buffer 89 buf.WriteString(fmt.Sprintf("Compiled Appfile: %s\n\n", c.File.Path)) 90 buf.WriteString("Dep Graph:\n") 91 buf.WriteString(c.Graph.String()) 92 buf.WriteString("\n") 93 return buf.String() 94 } 95 96 // CompiledGraphVertex is the type of the vertex within the Graph of Compiled. 97 type CompiledGraphVertex struct { 98 // File is the raw Appfile that this represents 99 File *File 100 101 // Dir is the directory of the data root for this dependency. This 102 // is only non-empty for dependencies (the root vertex does not have 103 // this value). 104 Dir string 105 106 // Don't use this outside of this package. 107 NameValue string 108 } 109 110 func (v *CompiledGraphVertex) Name() string { 111 return v.NameValue 112 } 113 114 // CompileOpts are the options for compilation. 115 type CompileOpts struct { 116 // Dir is the directory where all the compiled data will be stored. 117 // For use of Otto with a compiled Appfile, this directory must not 118 // be deleted. 119 Dir string 120 121 // Loader is called to load an Appfile in the given directory. 122 // This can return the file as-is, but this point gives the caller 123 // an opportunity to modify the Appfile prior to full compilation. 124 // 125 // The File given will already have all the imports merged. 126 Loader func(f *File, dir string) (*File, error) 127 128 // Callback is an optional way to receive notifications of events 129 // during the compilation process. The CompileEvent argument should be 130 // type switched to determine what it is. 131 Callback func(CompileEvent) 132 } 133 134 // Compiler is responsible for compiling Appfiles. For each instance 135 // of the compiler, the directory where Appfile data is stored is cleared 136 // and reloaded. 137 // 138 // Multiple calls to Compile can be made with a single Appfile and the 139 // dependencies won't be reloaded. 140 type Compiler struct { 141 opts *CompileOpts 142 depStorage getter.Storage 143 importCache map[string]*File 144 importLock sync.Mutex 145 importStorage getter.Storage 146 } 147 148 // CompileEvent is a potential event that a Callback can receive during 149 // Compilation. 150 type CompileEvent interface{} 151 152 // CompileEventDep is the event that is called when a dependency is 153 // being loaded. 154 type CompileEventDep struct { 155 Source string 156 } 157 158 // CompileEventImport is the event that is called when an import statement 159 // is being loaded and merged. 160 type CompileEventImport struct { 161 Source string 162 } 163 164 // LoadCompiled loads and verifies a compiled Appfile (*Compiled) from 165 // disk. 166 func LoadCompiled(dir string) (*Compiled, error) { 167 // Check the version 168 vsnStr, err := oneline.Read(filepath.Join(dir, CompileVersionFilename)) 169 if err != nil { 170 return nil, err 171 } 172 vsn, err := strconv.ParseInt(vsnStr, 0, 0) 173 if err != nil { 174 return nil, err 175 } 176 177 // If the version is too new, then we can't handle it 178 if vsn > CompileVersion { 179 return nil, fmt.Errorf( 180 "The Appfile for this enviroment was compiled with a newer version\n" + 181 "of Otto. Otto can't load this environment. You can recompile this\n" + 182 "environment to this version of Otto with `otto compile`.") 183 } 184 185 f, err := os.Open(filepath.Join(dir, CompileFilename)) 186 if err != nil { 187 return nil, err 188 } 189 defer f.Close() 190 191 var c Compiled 192 dec := json.NewDecoder(f) 193 if err := dec.Decode(&c); err != nil { 194 return nil, err 195 } 196 197 return &c, nil 198 } 199 200 // NewCompiler initializes a compiler with the given options. 201 func NewCompiler(opts *CompileOpts) (*Compiler, error) { 202 // Create the directory if it doesn't already exist 203 if err := os.MkdirAll(opts.Dir, 0755); err != nil { 204 return nil, err 205 } 206 207 // Setup our result 208 c := &Compiler{opts: opts} 209 210 // Setup our import storage and locks 211 c.importCache = make(map[string]*File) 212 c.importStorage = &getter.FolderStorage{ 213 StorageDir: filepath.Join(opts.Dir, CompileImportsFolder)} 214 215 // Setup dep storage 216 c.depStorage = &getter.FolderStorage{ 217 StorageDir: filepath.Join(opts.Dir, CompileDepsFolder)} 218 return c, nil 219 } 220 221 // Compile compiles an Appfile. 222 // 223 // This may require network connectivity if there are imports or 224 // non-local dependencies. The repositories that dependencies point to 225 // will be fully loaded into the given directory, and the compiled Appfile 226 // will be saved there. 227 // 228 // LoadCompiled can be used to load a pre-compiled Appfile. 229 // 230 // If you have no interest in reloading a compiled Appfile, you can 231 // recursively delete the compilation directory after this is completed. 232 // Note that certain functions of Otto such as development environments 233 // will depend on those directories existing, however. 234 func (c *Compiler) Compile(f *File) (*Compiled, error) { 235 // Write the version of the compilation that we'll be completing. 236 if err := compileVersion(c.opts.Dir); err != nil { 237 return nil, fmt.Errorf("Error writing compiled Appfile version: %s", err) 238 } 239 240 // Check if we have an ID for this or not. If we don't, then we need 241 // to write the ID file. We only do this if the file has a path. 242 if f.Path != "" { 243 hasID, err := f.hasID() 244 if err != nil { 245 return nil, fmt.Errorf( 246 "Error checking for Appfile UUID: %s", err) 247 } 248 249 if !hasID { 250 if err := f.initID(); err != nil { 251 return nil, fmt.Errorf( 252 "Error writing UUID for this Appfile: %s", err) 253 } 254 } 255 256 if err := f.loadID(); err != nil { 257 return nil, fmt.Errorf( 258 "Error loading Appfile UUID: %s", err) 259 } 260 } 261 262 // Do a minimum compile to start 263 compiled, err := c.MinCompile(f) 264 if err != nil { 265 return nil, err 266 } 267 268 // Validate the root early 269 if err := compiled.File.Validate(); err != nil { 270 return nil, err 271 } 272 273 // Get our root vertex 274 root, err := compiled.Graph.Root() 275 if err != nil { 276 return nil, err 277 } 278 vertex := root.(*CompiledGraphVertex) 279 280 // Build the storage we'll use for storing downloaded dependencies, 281 // then use that to trigger the recursive call to download all our 282 // dependencies. 283 if err := c.compileDependencies(vertex, compiled.Graph); err != nil { 284 return nil, err 285 } 286 287 // Validate the compiled file tree. 288 if err := compiled.Validate(); err != nil { 289 return nil, err 290 } 291 292 // Write the compiled Appfile data 293 if err := compileWrite(c.opts.Dir, compiled); err != nil { 294 return nil, err 295 } 296 297 return compiled, nil 298 } 299 300 // MinCompile does a minimal compilation of the given Appfile. 301 // 302 // This will load and merge any imports. This is used for a very basic 303 // Compiled Appfile that can be used with Otto core. 304 // 305 // This does not fetch dependencies. 306 func (c *Compiler) MinCompile(f *File) (*Compiled, error) { 307 // Start building our compiled Appfile 308 compiled := &Compiled{File: f, Graph: new(dag.AcyclicGraph)} 309 310 // Load the imports for this single Appfile 311 if err := c.compileImports(f); err != nil { 312 return nil, err 313 } 314 315 // Add our root vertex for this Appfile 316 vertex := &CompiledGraphVertex{File: f, NameValue: f.Application.Name} 317 compiled.Graph.Add(vertex) 318 319 return compiled, nil 320 } 321 322 func (c *Compiler) compileDependencies(root *CompiledGraphVertex, graph *dag.AcyclicGraph) error { 323 // For easier reference below 324 storage := c.depStorage 325 326 // Make a map to keep track of the dep source to vertex mapping 327 vertexMap := make(map[string]*CompiledGraphVertex) 328 329 // Store ourselves in the map 330 key, err := getter.Detect( 331 ".", filepath.Dir(root.File.Path), 332 getter.Detectors) 333 if err != nil { 334 return err 335 } 336 vertexMap[key] = root 337 338 // Make a queue for the other vertices we need to still get 339 // dependencies for. We arbitrarily make the cap for this slice 340 // 30, since that is a ton of dependencies and we don't expect the 341 // average case to have more than this. 342 queue := make([]*CompiledGraphVertex, 1, 30) 343 queue[0] = root 344 345 // While we still have dependencies to get, continue loading them. 346 // TODO: parallelize 347 for len(queue) > 0 { 348 var current *CompiledGraphVertex 349 current, queue = queue[len(queue)-1], queue[:len(queue)-1] 350 351 log.Printf("[DEBUG] compiling dependencies for: %s", current.Name()) 352 for _, dep := range current.File.Application.Dependencies { 353 key, err := getter.Detect( 354 dep.Source, filepath.Dir(current.File.Path), 355 getter.Detectors) 356 if err != nil { 357 return fmt.Errorf( 358 "Error loading source: %s", err) 359 } 360 361 vertex := vertexMap[key] 362 if vertex == nil { 363 log.Printf("[DEBUG] loading dependency: %s", key) 364 365 // Call the callback if we have one 366 if c.opts.Callback != nil { 367 c.opts.Callback(&CompileEventDep{ 368 Source: key, 369 }) 370 } 371 372 // Download the dependency 373 if err := storage.Get(key, key, true); err != nil { 374 return err 375 } 376 dir, _, err := storage.Dir(key) 377 if err != nil { 378 return err 379 } 380 381 // Parse the Appfile if it exists 382 var f *File 383 appfilePath := filepath.Join(dir, "Appfile") 384 _, err = os.Stat(appfilePath) 385 if err != nil && !os.IsNotExist(err) { 386 return fmt.Errorf( 387 "Error parsing Appfile in %s: %s", key, err) 388 } 389 if err == nil { 390 f, err = ParseFile(appfilePath) 391 if err != nil { 392 return fmt.Errorf( 393 "Error parsing Appfile in %s: %s", key, err) 394 } 395 396 // Realize all the imports for this file 397 if err := c.compileImports(f); err != nil { 398 return err 399 } 400 } 401 402 // Do any additional loading if we have a loader 403 if c.opts.Loader != nil { 404 f, err = c.opts.Loader(f, dir) 405 if err != nil { 406 return fmt.Errorf( 407 "Error loading Appfile in %s: %s", key, err) 408 } 409 } 410 411 // Set the source 412 f.Source = key 413 414 // If it doesn't have an otto ID then we can't do anything 415 hasID, err := f.hasID() 416 if err != nil { 417 return fmt.Errorf( 418 "Error checking for ID file for Appfile in %s: %s", 419 key, err) 420 } 421 if !hasID { 422 return fmt.Errorf( 423 "Dependency '%s' doesn't have an Otto ID yet!\n\n"+ 424 "An Otto ID is generated on the first compilation of the Appfile.\n"+ 425 "It is a globally unique ID that is used to track the application\n"+ 426 "across multiple deploys. It is required for the application to be\n"+ 427 "used as a dependency. To fix this, check out that application and\n"+ 428 "compile the Appfile with `otto compile` once. Make sure you commit\n"+ 429 "the .ottoid file into version control, and then try this command\n"+ 430 "again.", 431 key) 432 } 433 434 // We merge the root infrastructure choice upwards to 435 // all dependencies. 436 f.Infrastructure = root.File.Infrastructure 437 if root.File.Project != nil { 438 if f.Project == nil { 439 f.Project = new(Project) 440 } 441 f.Project.Infrastructure = root.File.Project.Infrastructure 442 } 443 444 // Build the vertex for this 445 vertex = &CompiledGraphVertex{ 446 File: f, 447 Dir: dir, 448 NameValue: f.Application.Name, 449 } 450 451 // Add the vertex since it is new, store the mapping, and 452 // queue it to be loaded later. 453 graph.Add(vertex) 454 vertexMap[key] = vertex 455 queue = append(queue, vertex) 456 } 457 458 // Connect the dependencies 459 graph.Connect(dag.BasicEdge(current, vertex)) 460 } 461 } 462 463 return nil 464 } 465 466 type compileImportOpts struct { 467 Storage getter.Storage 468 Cache map[string]*File 469 CacheLock *sync.Mutex 470 } 471 472 // compileImports takes a File, loads all the imports, and merges them 473 // into the File. 474 func (c *Compiler) compileImports(root *File) error { 475 // If we have no imports, short-circuit the whole thing 476 if len(root.Imports) == 0 { 477 return nil 478 } 479 480 // Pull these out into variables so they're easier to reference 481 storage := c.importStorage 482 cache := c.importCache 483 cacheLock := &c.importLock 484 485 // A graph is used to track for cycles 486 var graphLock sync.Mutex 487 graph := new(dag.AcyclicGraph) 488 graph.Add("root") 489 490 // Since we run the import in parallel, multiple errors can happen 491 // at the same time. We use multierror and a lock to keep track of errors. 492 var resultErr error 493 var resultErrLock sync.Mutex 494 495 // Forward declarations for some nested functions we use. The docs 496 // for these functions are above each. 497 var importSingle func(parent string, f *File) bool 498 var downloadSingle func(string, *sync.WaitGroup, *sync.Mutex, []*File, int) 499 500 // importSingle is responsible for kicking off the imports and merging 501 // them for a single file. This will return true on success, false on 502 // failure. On failure, it is expected that any errors are appended to 503 // resultErr. 504 importSingle = func(parent string, f *File) bool { 505 var wg sync.WaitGroup 506 507 // Build the list of files we'll merge later 508 var mergeLock sync.Mutex 509 merge := make([]*File, len(f.Imports)) 510 511 // Go through the imports and kick off the download 512 for idx, i := range f.Imports { 513 source, err := getter.Detect( 514 i.Source, filepath.Dir(f.Path), 515 getter.Detectors) 516 if err != nil { 517 resultErrLock.Lock() 518 defer resultErrLock.Unlock() 519 resultErr = multierror.Append(resultErr, fmt.Errorf( 520 "Error loading import source: %s", err)) 521 return false 522 } 523 524 // Add this to the graph and check now if there are cycles 525 graphLock.Lock() 526 graph.Add(source) 527 graph.Connect(dag.BasicEdge(parent, source)) 528 cycles := graph.Cycles() 529 graphLock.Unlock() 530 if len(cycles) > 0 { 531 for _, cycle := range cycles { 532 names := make([]string, len(cycle)) 533 for i, v := range cycle { 534 names[i] = dag.VertexName(v) 535 } 536 537 resultErrLock.Lock() 538 defer resultErrLock.Unlock() 539 resultErr = multierror.Append(resultErr, fmt.Errorf( 540 "Cycle found: %s", strings.Join(names, ", "))) 541 return false 542 } 543 } 544 545 wg.Add(1) 546 go downloadSingle(source, &wg, &mergeLock, merge, idx) 547 } 548 549 // Wait for completion 550 wg.Wait() 551 552 // Go through the merge list and look for any nil entries, which 553 // means that download failed. In that case, return immediately. 554 // We assume any errors were put into resultErr. 555 for _, importF := range merge { 556 if importF == nil { 557 return false 558 } 559 } 560 561 for _, importF := range merge { 562 // We need to copy importF here so that we don't poison 563 // the cache by modifying the same pointer. 564 importFCopy := *importF 565 importF = &importFCopy 566 source := importF.ID 567 importF.ID = "" 568 importF.Path = "" 569 570 // Merge it into our file! 571 if err := f.Merge(importF); err != nil { 572 resultErrLock.Lock() 573 defer resultErrLock.Unlock() 574 resultErr = multierror.Append(resultErr, fmt.Errorf( 575 "Error merging import %s: %s", source, err)) 576 return false 577 } 578 } 579 580 return true 581 } 582 583 // downloadSingle is used to download a single import and parse the 584 // Appfile. This is a separate function because it is generally run 585 // in a goroutine so we can parallelize grabbing the imports. 586 downloadSingle = func(source string, wg *sync.WaitGroup, l *sync.Mutex, result []*File, idx int) { 587 defer wg.Done() 588 589 // Read from the cache if we have it 590 cacheLock.Lock() 591 cached, ok := cache[source] 592 cacheLock.Unlock() 593 if ok { 594 log.Printf("[DEBUG] cache hit on import: %s", source) 595 l.Lock() 596 defer l.Unlock() 597 result[idx] = cached 598 return 599 } 600 601 // Call the callback if we have one 602 log.Printf("[DEBUG] loading import: %s", source) 603 if c.opts.Callback != nil { 604 c.opts.Callback(&CompileEventImport{ 605 Source: source, 606 }) 607 } 608 609 // Download the dependency 610 if err := storage.Get(source, source, true); err != nil { 611 resultErrLock.Lock() 612 defer resultErrLock.Unlock() 613 resultErr = multierror.Append(resultErr, fmt.Errorf( 614 "Error loading import source: %s", err)) 615 return 616 } 617 dir, _, err := storage.Dir(source) 618 if err != nil { 619 resultErrLock.Lock() 620 defer resultErrLock.Unlock() 621 resultErr = multierror.Append(resultErr, fmt.Errorf( 622 "Error loading import source: %s", err)) 623 return 624 } 625 626 // Parse the Appfile 627 importF, err := ParseFile(filepath.Join(dir, "Appfile")) 628 if err != nil { 629 resultErrLock.Lock() 630 defer resultErrLock.Unlock() 631 resultErr = multierror.Append(resultErr, fmt.Errorf( 632 "Error parsing Appfile in %s: %s", source, err)) 633 return 634 } 635 636 // We use the ID to store the source, but we clear it 637 // when we actually merge. 638 importF.ID = source 639 640 // Import the imports in this 641 if !importSingle(source, importF) { 642 return 643 } 644 645 // Once we're done, acquire the lock and write it 646 l.Lock() 647 result[idx] = importF 648 l.Unlock() 649 650 // Write this into the cache. 651 cacheLock.Lock() 652 cache[source] = importF 653 cacheLock.Unlock() 654 } 655 656 importSingle("root", root) 657 return resultErr 658 } 659 660 func compileVersion(dir string) error { 661 f, err := os.Create(filepath.Join(dir, CompileVersionFilename)) 662 if err != nil { 663 return err 664 } 665 defer f.Close() 666 667 _, err = fmt.Fprintf(f, "%d", CompileVersion) 668 return err 669 } 670 671 func compileWrite(dir string, compiled *Compiled) error { 672 // Pretty-print the JSON data so that it can be more easily inspected 673 data, err := json.MarshalIndent(compiled, "", " ") 674 if err != nil { 675 return err 676 } 677 678 // Write it out 679 f, err := os.Create(filepath.Join(dir, CompileFilename)) 680 if err != nil { 681 return err 682 } 683 defer f.Close() 684 685 _, err = io.Copy(f, bytes.NewReader(data)) 686 return err 687 }