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  }