github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/command/meta.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/hashicorp/otto/appfile"
    12  	"github.com/hashicorp/otto/directory"
    13  	"github.com/hashicorp/otto/otto"
    14  	"github.com/hashicorp/otto/plugin"
    15  	"github.com/hashicorp/otto/ui"
    16  	"github.com/mitchellh/cli"
    17  	"github.com/mitchellh/go-homedir"
    18  )
    19  
    20  const (
    21  	// DefaultAppfile is the default filename for the Appfile
    22  	DefaultAppfile = "Appfile"
    23  
    24  	// DefaultLocalDataDir is the default path to the local data
    25  	// directory.
    26  	DefaultLocalDataDir         = "~/.otto.d"
    27  	DefaultLocalDataDetectorDir = "detect"
    28  	DefaultLocalDataPluginsDir  = "plugins"
    29  
    30  	// DefaultOutputDir is the default filename for the output directory
    31  	DefaultOutputDir                = ".otto"
    32  	DefaultOutputDirCompiledAppfile = "appfile"
    33  	DefaultOutputDirCompiledData    = "compiled"
    34  	DefaultOutputDirLocalData       = "data"
    35  
    36  	// DefaultDataDir is the default directory for the directory
    37  	// data if a directory in the Appfile isn't specified.
    38  	DefaultDataDir = "otto-data"
    39  )
    40  
    41  var (
    42  	// AltAppfiles is the list of alternative names for an Appfile that Otto can
    43  	// detect and load automatically
    44  	AltAppfiles = []string{"appfile.hcl"}
    45  )
    46  
    47  // FlagSetFlags is an enum to define what flags are present in the
    48  // default FlagSet returned by Meta.FlagSet
    49  type FlagSetFlags uint
    50  
    51  const (
    52  	FlagSetNone FlagSetFlags = 0
    53  )
    54  
    55  // Meta are the meta-options that are available on all or most commands.
    56  type Meta struct {
    57  	CoreConfig *otto.CoreConfig
    58  	Ui         cli.Ui
    59  	PluginMap  plugin.ServeMuxMap
    60  
    61  	pluginManager *PluginManager
    62  }
    63  
    64  // Appfile loads the compiled Appfile. If the Appfile isn't compiled yet,
    65  // then an error will be returned.
    66  func (m *Meta) Appfile() (*appfile.Compiled, error) {
    67  	// Find the root directory
    68  	startDir, err := os.Getwd()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	rootDir, err := m.RootDir(startDir)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	return appfile.LoadCompiled(filepath.Join(
    78  		rootDir, DefaultOutputDir, DefaultOutputDirCompiledAppfile))
    79  }
    80  
    81  // Core returns the core for the given Appfile. The file where the
    82  // Appfile was loaded from should be set in appfile.File.Path. This
    83  // root appfile path will be used as the default output directory
    84  // for Otto.
    85  func (m *Meta) Core(f *appfile.Compiled) (*otto.Core, error) {
    86  	if f.File == nil || f.File.Path == "" {
    87  		return nil, fmt.Errorf("Could not determine Appfile dir")
    88  	}
    89  
    90  	rootDir, err := m.RootDir(filepath.Dir(f.File.Path))
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	rootDir, err = filepath.Abs(rootDir)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	dataDir, err := m.DataDir()
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	pluginMgr, err := m.PluginManager()
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	if len(pluginMgr.Plugins()) == 0 {
   110  		// We haven't loaded any plugins. Look for them in the
   111  		// used directory and load them.
   112  		usedPath, err := m.AppfilePluginsPath(f)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		if _, err := os.Stat(usedPath); err == nil {
   117  			if err := pluginMgr.LoadUsed(usedPath); err != nil {
   118  				return nil, err
   119  			}
   120  		}
   121  	}
   122  
   123  	// Configure the core with what we have from the plugin manager.
   124  	if err := pluginMgr.ConfigureCore(m.CoreConfig); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	config := *m.CoreConfig
   129  	config.Appfile = f
   130  	config.DataDir = dataDir
   131  	config.LocalDir = filepath.Join(
   132  		rootDir, DefaultOutputDir, DefaultOutputDirLocalData)
   133  	config.CompileDir = filepath.Join(
   134  		rootDir, DefaultOutputDir, DefaultOutputDirCompiledData)
   135  	config.Ui = m.OttoUi()
   136  
   137  	config.Directory, err = m.Directory(&config)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	return otto.NewCore(&config)
   143  }
   144  
   145  // AppfilePluginsPath returns the path where the used plugins data
   146  // should be stored based on an Appfile.
   147  func (m *Meta) AppfilePluginsPath(f *appfile.Compiled) (string, error) {
   148  	rootDir, err := m.RootDir(filepath.Dir(f.File.Path))
   149  	if err != nil {
   150  		return "", err
   151  	}
   152  
   153  	rootDir, err = filepath.Abs(rootDir)
   154  	if err != nil {
   155  		return "", err
   156  	}
   157  
   158  	return filepath.Join(
   159  		rootDir, DefaultOutputDir, DefaultOutputDirCompiledAppfile, "plugins.json"), nil
   160  }
   161  
   162  // DataDir returns the user-local data directory for Otto.
   163  func (m *Meta) DataDir() (string, error) {
   164  	return homedir.Expand(DefaultLocalDataDir)
   165  }
   166  
   167  // RootDir finds the "root" directory. This is the working directory of
   168  // the Appfile and Otto itself. To find the root directory, we traverse
   169  // upwards until we find the ".otto" directory and assume that is where
   170  // it is.
   171  func (m *Meta) RootDir(startDir string) (string, error) {
   172  	current := startDir
   173  
   174  	// Traverse upwards until we find the directory. We also protect this
   175  	// loop with a basic infinite loop guard.
   176  	i := 0
   177  	prev := ""
   178  	for prev != current && i < 1000 {
   179  		if _, err := os.Stat(filepath.Join(current, DefaultOutputDir)); err == nil {
   180  			// Found it
   181  			return current, nil
   182  		}
   183  
   184  		prev = current
   185  		current = filepath.Dir(current)
   186  		i++
   187  	}
   188  
   189  	return "", fmt.Errorf(
   190  		"Otto doesn't appear to have compiled your Appfile yet!\n\n" +
   191  			"Run `otto compile` in the directory with the Appfile or\n" +
   192  			"with the `-appfile` flag in order to compile the files for\n" +
   193  			"developing, building, and deploying your application.\n\n" +
   194  			"Once the Appfile is compiled, you can run `otto` in any\n" +
   195  			"subdirectory.")
   196  }
   197  
   198  // Directory returns the Otto directory backend for the given
   199  // Appfile. If no directory backend is specified, a local folder
   200  // will be used.
   201  func (m *Meta) Directory(config *otto.CoreConfig) (directory.Backend, error) {
   202  	return &directory.BoltBackend{
   203  		Dir: filepath.Join(config.DataDir, "directory"),
   204  	}, nil
   205  }
   206  
   207  // FlagSet returns a FlagSet with the common flags that every
   208  // command implements. The exact behavior of FlagSet can be configured
   209  // using the flags as the second parameter.
   210  func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
   211  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   212  
   213  	// Create an io.Writer that writes to our Ui properly for errors.
   214  	// This is kind of a hack, but it does the job. Basically: create
   215  	// a pipe, use a scanner to break it into lines, and output each line
   216  	// to the UI. Do this forever.
   217  	errR, errW := io.Pipe()
   218  	errScanner := bufio.NewScanner(errR)
   219  	go func() {
   220  		for errScanner.Scan() {
   221  			m.Ui.Error(errScanner.Text())
   222  		}
   223  	}()
   224  	f.SetOutput(errW)
   225  
   226  	return f
   227  }
   228  
   229  // PluginManager returns the PluginManager configured with the proper
   230  // directories for this command invocation.
   231  //
   232  // This is a singleton for each Meta, so multiple calls will return the
   233  // same object.
   234  func (m *Meta) PluginManager() (*PluginManager, error) {
   235  	if m.pluginManager != nil {
   236  		return m.pluginManager, nil
   237  	}
   238  
   239  	// Get the root directory to look for plugins. If we can't get it,
   240  	// then assume we're in the pwd for compilation.
   241  	startDir, err := os.Getwd()
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	rootDir, err := m.RootDir(startDir)
   246  	if err != nil {
   247  		rootDir = startDir
   248  	}
   249  
   250  	// Data directory where plugins can be
   251  	dataDir, err := m.DataDir()
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	m.pluginManager = &PluginManager{
   257  		PluginMap: m.PluginMap,
   258  		PluginDirs: []string{
   259  			rootDir,
   260  			filepath.Join(dataDir, DefaultLocalDataPluginsDir),
   261  			filepath.Dir(pluginExePath),
   262  		},
   263  	}
   264  	return m.pluginManager, nil
   265  }
   266  
   267  // OttoUi returns the ui.Ui object.
   268  func (m *Meta) OttoUi() ui.Ui {
   269  	return NewUi(m.Ui)
   270  }
   271  
   272  // confirmDestroy is a little helper that will ask the user to confirm a
   273  // destroy action using the provided msg, unless -force is included in args it
   274  // returns true if the destroy should be considered confirmed, and false if
   275  // the destroy should be aborted.
   276  func (m *Meta) confirmDestroy(msg string, args []string) bool {
   277  	destroyForce := false
   278  	for _, arg := range args {
   279  		if arg == "-force" {
   280  			destroyForce = true
   281  		}
   282  	}
   283  
   284  	if !destroyForce {
   285  		v, err := m.OttoUi().Input(&ui.InputOpts{
   286  			Id:    "destroy",
   287  			Query: "Do you really want to destroy?",
   288  			Description: fmt.Sprintf("%s\n"+
   289  				"There is no undo. Only 'yes' will be accepted to confirm.", msg),
   290  		})
   291  		if err != nil {
   292  			m.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
   293  			return false
   294  		}
   295  		if v != "yes" {
   296  			m.Ui.Output("Destroy cancelled.")
   297  			return false
   298  		}
   299  	}
   300  
   301  	return true
   302  }