github.com/mmatczuk/gohan@v0.0.0-20170206152520-30e45d9bdb69/cli/cli.go (about)

     1  // Copyright (C) 2015 NTT Innovation Institute, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    12  // implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package cli
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/cloudwan/gohan/schema"
    29  	"github.com/cloudwan/gohan/util"
    30  	"github.com/lestrrat/go-server-starter"
    31  
    32  	"github.com/cloudwan/gohan/cli/client"
    33  	"github.com/cloudwan/gohan/db"
    34  	"github.com/cloudwan/gohan/db/sql"
    35  	"github.com/cloudwan/gohan/extension/framework"
    36  	"github.com/cloudwan/gohan/server"
    37  	"github.com/codegangsta/cli"
    38  
    39  	"github.com/cloudwan/gohan/extension/gohanscript"
    40  	//Import gohan script lib
    41  	_ "github.com/cloudwan/gohan/extension/gohanscript/autogen"
    42  	l "github.com/cloudwan/gohan/log"
    43  )
    44  
    45  var log = l.NewLogger()
    46  
    47  const defaultConfigFile = "gohan.yaml"
    48  
    49  //Run execute main command
    50  func Run(name, usage, version string) {
    51  	app := cli.NewApp()
    52  	app.Name = "gohan"
    53  	app.Usage = "Gohan"
    54  	app.Version = version
    55  	app.Flags = []cli.Flag{
    56  		cli.BoolFlag{Name: "debug, d", Usage: "Show debug messages"},
    57  	}
    58  	app.Before = parseGlobalOptions
    59  	app.Commands = []cli.Command{
    60  		getGohanClientCommand(),
    61  		getValidateCommand(),
    62  		getInitDbCommand(),
    63  		getConvertCommand(),
    64  		getServerCommand(),
    65  		getTestExtesionsCommand(),
    66  		getMigrateCommand(),
    67  		getTemplateCommand(),
    68  		getRunCommand(),
    69  		getTestCommand(),
    70  		getOpenAPICommand(),
    71  		getMarkdownCommand(),
    72  		getDotCommand(),
    73  		getGraceServerCommand(),
    74  	}
    75  	app.Run(os.Args)
    76  }
    77  
    78  func parseGlobalOptions(c *cli.Context) (err error) {
    79  	//TODO(marcin) do it
    80  	return nil
    81  }
    82  
    83  func getGohanClientCommand() cli.Command {
    84  	return cli.Command{
    85  		Name:            "client",
    86  		Usage:           "Manage Gohan resources",
    87  		SkipFlagParsing: true,
    88  		HideHelp:        true,
    89  		Description: `gohan client schema_id command [arguments...]
    90  
    91  COMMANDS:
    92      list                List all resources
    93      show                Show resource details
    94      create              Create resource
    95      set                 Update resource
    96      delete              Delete resource
    97  
    98  ARGUMENTS:
    99      There are two types of arguments:
   100          - named:
   101              they are in '--name value' format and should be specified directly after
   102              command name.
   103              If you want to pass JSON null value, it should be written as: '--name "<null>"'.
   104  
   105              Special named arguments:
   106                  --output-format [json/table] - specifies in which format results should be shown
   107                  --verbosity [0-3] - specifies how much debug info Gohan Client should show (default 0)
   108          - unnamed:
   109              they are in 'value' format and should be specified at the end of the line,
   110              after all named arguments. At the moment only 'id' argument in 'show',
   111              'set' and 'delete' commands are available in this format.
   112  
   113      Identifying resources:
   114          It is possible to identify resources with its ID and Name.
   115  
   116          Examples:
   117              gohan client network show network-id
   118              gohan client network show "Network Name"
   119              gohan client subnet create --network "Network Name"
   120              gohan client subnet create --network network-id
   121              gohan client subnet create --network_id network-id
   122  
   123  CONFIGURATION:
   124      Configuration is available by environment variables:
   125          * Keystone username - OS_USERNAME
   126          * Keystone password - OS_PASSWORD
   127          * Keystone tenant name or tenant id - OS_TENANT_NAME or OS_TENANT_ID
   128          * Keystone url - OS_AUTH_URL
   129          * Gohan service name in keystone - GOHAN_SERVICE_NAME
   130          * Gohan region in keystone - GOHAN_REGION
   131          * Gohan schema URL - GOHAN_SCHEMA_URL
   132          * Should Client cache schemas (default - true) - GOHAN_CACHE_SCHEMAS
   133          * Cache expiration time (in format 1h20m10s - default 5m) - GOHAN_CACHE_TIMEOUT
   134          * Cache path (default - /tmp/.cached-gohan-schemas) - GOHAN_CACHE_PATH
   135          * In which format results should be shown, see --output-format - GOHAN_OUTPUT_FORMAT
   136          * How much debug info Gohan Client should show, see --verbosity - GOHAN_VERBOSITY
   137      Additional options for Keystone v3 only:
   138          * Keystone domain name or domain id - OS_DOMAIN_NAME or OS_DOMAIN_ID
   139  `,
   140  		Action: func(c *cli.Context) {
   141  			opts, err := client.NewOptsFromEnv()
   142  			if err != nil {
   143  				util.ExitFatal(err)
   144  			}
   145  
   146  			gohanCLI, err := client.NewGohanClientCLI(opts)
   147  			if err != nil {
   148  				util.ExitFatalf("Error initializing Gohan Client CLI: %v\n", err)
   149  			}
   150  
   151  			command := fmt.Sprintf("%s %s", c.Args().Get(0), c.Args().Get(1))
   152  			arguments := c.Args().Tail()
   153  			if len(arguments) > 0 {
   154  				arguments = arguments[1:]
   155  			}
   156  			result, err := gohanCLI.ExecuteCommand(command, arguments)
   157  			if err != nil {
   158  				util.ExitFatal(err)
   159  			}
   160  			if result == "null" {
   161  				result = ""
   162  			}
   163  			fmt.Println(result)
   164  		},
   165  	}
   166  }
   167  
   168  func getValidateCommand() cli.Command {
   169  	return cli.Command{
   170  		Name:      "validate",
   171  		ShortName: "v",
   172  		Usage:     "Validate document",
   173  		Description: `
   174  Validate document against schema.
   175  It's especially useful to validate schema files against gohan meta-schema.`,
   176  		Flags: []cli.Flag{
   177  			cli.StringFlag{Name: "schema, s", Value: "etc/schema/gohan.json", Usage: "Schema path"},
   178  			cli.StringSliceFlag{Name: "document, d", Usage: "Document path"},
   179  		},
   180  		Action: func(c *cli.Context) {
   181  			schemaPath := c.String("schema")
   182  			documentPaths := c.StringSlice("document")
   183  			if len(documentPaths) == 0 {
   184  				util.ExitFatalf("At least one document should be specified for validation\n")
   185  			}
   186  
   187  			manager := schema.GetManager()
   188  			err := manager.LoadSchemaFromFile(schemaPath)
   189  			if err != nil {
   190  				util.ExitFatal("Failed to parse schema:", err)
   191  			}
   192  
   193  			for _, documentPath := range documentPaths {
   194  				err = manager.LoadSchemaFromFile(documentPath)
   195  				if err != nil {
   196  					util.ExitFatalf("Schema is not valid, see errors below:\n%s\n", err)
   197  				}
   198  			}
   199  			fmt.Println("Schema is valid")
   200  		},
   201  	}
   202  }
   203  
   204  func getInitDbCommand() cli.Command {
   205  	return cli.Command{
   206  		Name:      "init-db",
   207  		ShortName: "idb",
   208  		Usage:     "Initialize DB backend with given schema file",
   209  		Description: `
   210  Initialize empty database with given schema.
   211  
   212  Setting meta-schema option will additionally populate meta-schema table with schema resources.
   213  Useful for development purposes.`,
   214  		Flags: []cli.Flag{
   215  			cli.StringFlag{Name: "database-type, t", Value: "sqlite3", Usage: "Backend datebase type"},
   216  			cli.StringFlag{Name: "database, d", Value: "gohan.db", Usage: "DB connection string"},
   217  			cli.StringFlag{Name: "schema, s", Value: "embed://etc/schema/gohan.json", Usage: "Schema definition"},
   218  			cli.BoolFlag{Name: "drop-on-create", Usage: "If true, old database will be dropped"},
   219  			cli.BoolFlag{Name: "cascade", Usage: "If true, FOREIGN KEYS in database will be created with ON DELETE CASCADE"},
   220  			cli.StringFlag{Name: "meta-schema, m", Value: "", Usage: "Meta-schema file (optional)"},
   221  		},
   222  		Action: func(c *cli.Context) {
   223  			dbType := c.String("database-type")
   224  			dbConnection := c.String("database")
   225  			schemaFile := c.String("schema")
   226  			metaSchemaFile := c.String("meta-schema")
   227  			dropOnCreate := c.Bool("drop-on-create")
   228  			cascade := c.Bool("cascade")
   229  			manager := schema.GetManager()
   230  			manager.LoadSchemasFromFiles(schemaFile, metaSchemaFile)
   231  			err := db.InitDBWithSchemas(dbType, dbConnection, dropOnCreate, cascade)
   232  			if err != nil {
   233  				util.ExitFatal(err)
   234  			}
   235  			fmt.Println("DB is initialized")
   236  		},
   237  	}
   238  }
   239  
   240  func getConvertCommand() cli.Command {
   241  	return cli.Command{
   242  		Name:      "convert",
   243  		ShortName: "conv",
   244  		Usage:     "Convert DB",
   245  		Description: `
   246  Gohan convert can be used to migrate Gohan resources between different types of databases.
   247  
   248  Setting meta-schema option will additionally convert meta-schema table with schema resources.
   249  Useful for development purposes.`,
   250  		Flags: []cli.Flag{
   251  			cli.StringFlag{Name: "in-type, it", Value: "", Usage: "Input db type (yaml, json, sqlite3, mysql)"},
   252  			cli.StringFlag{Name: "in, i", Value: "", Usage: "Input db connection spec (or filename)"},
   253  			cli.StringFlag{Name: "out-type, ot", Value: "", Usage: "Output db type (yaml, json, sqlite3, mysql)"},
   254  			cli.StringFlag{Name: "out, o", Value: "", Usage: "Output db connection spec (or filename)"},
   255  			cli.StringFlag{Name: "schema, s", Value: "", Usage: "Schema file"},
   256  			cli.StringFlag{Name: "meta-schema, m", Value: "embed://etc/schema/gohan.json", Usage: "Meta-schema file (optional)"},
   257  		},
   258  		Action: func(c *cli.Context) {
   259  			inType, in := c.String("in-type"), c.String("in")
   260  			if inType == "" || in == "" {
   261  				util.ExitFatal("Need to provide input database specification")
   262  			}
   263  			outType, out := c.String("out-type"), c.String("out")
   264  			if outType == "" || out == "" {
   265  				util.ExitFatal("Need to provide output database specification")
   266  			}
   267  
   268  			schemaFile := c.String("schema")
   269  			if schemaFile == "" {
   270  				util.ExitFatal("Need to provide schema file")
   271  			}
   272  			metaSchemaFile := c.String("meta-schema")
   273  
   274  			schemaManager := schema.GetManager()
   275  			err := schemaManager.LoadSchemasFromFiles(schemaFile, metaSchemaFile)
   276  			if err != nil {
   277  				util.ExitFatal("Error loading schema:", err)
   278  			}
   279  
   280  			inDB, err := db.ConnectDB(inType, in, db.DefaultMaxOpenConn)
   281  			if err != nil {
   282  				util.ExitFatal(err)
   283  			}
   284  			outDB, err := db.ConnectDB(outType, out, db.DefaultMaxOpenConn)
   285  			if err != nil {
   286  				util.ExitFatal(err)
   287  			}
   288  
   289  			err = db.CopyDBResources(inDB, outDB, true)
   290  			if err != nil {
   291  				util.ExitFatal(err)
   292  			}
   293  
   294  			fmt.Println("Conversion complete")
   295  		},
   296  	}
   297  }
   298  
   299  func getServerCommand() cli.Command {
   300  	return cli.Command{
   301  		Name:        "server",
   302  		ShortName:   "srv",
   303  		Usage:       "Run API Server",
   304  		Description: "Run Gohan API server",
   305  		Flags: []cli.Flag{
   306  			cli.StringFlag{Name: "config-file", Value: defaultConfigFile, Usage: "Server config File"},
   307  		},
   308  		Action: func(c *cli.Context) {
   309  			configFile := c.String("config-file")
   310  			server.RunServer(configFile)
   311  		},
   312  	}
   313  }
   314  
   315  func getTestExtesionsCommand() cli.Command {
   316  	return cli.Command{
   317  		Name:      "test_extensions",
   318  		ShortName: "test_ex",
   319  		Usage:     "Run extension tests",
   320  		Description: `
   321  Run extensions tests in a gohan-server-like environment.
   322  
   323  Test files and directories can be supplied as arguments. See Gohan
   324  documentation for detail information about writing tests.`,
   325  		Flags: []cli.Flag{
   326  			cli.BoolFlag{Name: "verbose, v", Usage: "Print logs for passing tests"},
   327  			cli.StringFlag{Name: "config-file,c", Value: "", Usage: "Config file path"},
   328  			cli.StringFlag{Name: "run-test,r", Value: "", Usage: "Run only tests matching specified regex"},
   329  			cli.IntFlag{Name: "parallel, p", Value: runtime.NumCPU(), Usage: "Allow parallel execution of test functions"},
   330  		},
   331  		Action: framework.TestExtensions,
   332  	}
   333  }
   334  
   335  func getMigrateCommand() cli.Command {
   336  	return cli.Command{
   337  		Name:        "migrate",
   338  		ShortName:   "mig",
   339  		Usage:       "Generate goose migration script",
   340  		Description: `Generates goose migraion script`,
   341  		Flags: []cli.Flag{
   342  			cli.StringFlag{Name: "name, n", Value: "init_schema", Usage: "name of migrate"},
   343  			cli.StringFlag{Name: "schema, s", Value: "", Usage: "Schema definition"},
   344  			cli.StringFlag{Name: "path, p", Value: "etc/db/migrations", Usage: "Migrate path"},
   345  			cli.BoolFlag{Name: "cascade", Usage: "If true, FOREIGN KEYS in database will be created with ON DELETE CASCADE"},
   346  		},
   347  		Action: func(c *cli.Context) {
   348  			schemaFile := c.String("schema")
   349  			cascade := c.Bool("cascade")
   350  			manager := schema.GetManager()
   351  			manager.LoadSchemasFromFiles(schemaFile)
   352  			name := c.String("name")
   353  			now := time.Now()
   354  			version := fmt.Sprintf("%s_%s.sql", now.Format("20060102150405"), name)
   355  			path := filepath.Join(c.String("path"), version)
   356  			var sqlString = bytes.NewBuffer(make([]byte, 0, 100))
   357  			fmt.Printf("Generating goose migration file to %s ...\n", path)
   358  			sqlDB := sql.NewDB()
   359  			schemas := manager.Schemas()
   360  			sqlString.WriteString("\n")
   361  			sqlString.WriteString("-- +goose Up\n")
   362  			sqlString.WriteString("-- SQL in section 'Up' is executed when this migration is applied\n")
   363  			for _, s := range schemas {
   364  				createSql, indices := sqlDB.GenTableDef(s, cascade)
   365  				sqlString.WriteString(createSql + "\n")
   366  				for _, indexSql := range indices {
   367  					sqlString.WriteString(indexSql + "\n")
   368  				}
   369  			}
   370  			sqlString.WriteString("\n")
   371  			sqlString.WriteString("-- +goose Down\n")
   372  			sqlString.WriteString("-- SQL section 'Down' is executed when this migration is rolled back\n")
   373  			for _, s := range schemas {
   374  				sqlString.WriteString(fmt.Sprintf("drop table %s;", s.GetDbTableName()))
   375  				sqlString.WriteString("\n\n")
   376  			}
   377  			err := ioutil.WriteFile(path, sqlString.Bytes(), os.ModePerm)
   378  			if err != nil {
   379  				fmt.Println(err)
   380  			}
   381  		},
   382  	}
   383  }
   384  
   385  func getRunCommand() cli.Command {
   386  	return cli.Command{
   387  		Name:      "run",
   388  		ShortName: "run",
   389  		Usage:     "Run Gohan script Code",
   390  		Description: `
   391  Run gohan script code.`,
   392  		Flags: []cli.Flag{
   393  			cli.StringFlag{Name: "config-file,c", Value: defaultConfigFile, Usage: "config file path"},
   394  			cli.StringFlag{Name: "args,a", Value: "", Usage: "arguments"},
   395  		},
   396  		Action: func(c *cli.Context) {
   397  			src := c.Args()[0]
   398  			vm := gohanscript.NewVM()
   399  
   400  			args := []interface{}{}
   401  			flags := map[string]interface{}{}
   402  			for _, arg := range c.Args()[1:] {
   403  				if strings.Contains(arg, "=") {
   404  					kv := strings.Split(arg, "=")
   405  					flags[kv[0]] = kv[1]
   406  				} else {
   407  					args = append(args, arg)
   408  				}
   409  			}
   410  			vm.Context.Set("args", args)
   411  			vm.Context.Set("flags", flags)
   412  			configFile := c.String("config-file")
   413  			loadConfig(configFile)
   414  			_, err := vm.RunFile(src)
   415  			if err != nil {
   416  				fmt.Println(err)
   417  				os.Exit(1)
   418  				return
   419  			}
   420  		},
   421  	}
   422  }
   423  
   424  func getTestCommand() cli.Command {
   425  	return cli.Command{
   426  		Name:      "test",
   427  		ShortName: "test",
   428  		Usage:     "Run Gohan script Test",
   429  		Description: `
   430  Run gohan script yaml code.`,
   431  		Flags: []cli.Flag{
   432  			cli.StringFlag{Name: "config-file,c", Value: defaultConfigFile, Usage: "config file path"},
   433  		},
   434  		Action: func(c *cli.Context) {
   435  			dir := c.Args()[0]
   436  			configFile := c.String("config-file")
   437  			loadConfig(configFile)
   438  			gohanscript.RunTests(dir)
   439  		},
   440  	}
   441  }
   442  
   443  func loadConfig(configFile string) {
   444  	if configFile == "" {
   445  		return
   446  	}
   447  	config := util.GetConfig()
   448  	err := config.ReadConfig(configFile)
   449  	if err != nil {
   450  		if configFile != defaultConfigFile {
   451  			fmt.Println(err)
   452  			os.Exit(1)
   453  		}
   454  		return
   455  	}
   456  	err = l.SetUpLogging(config)
   457  	if err != nil {
   458  		fmt.Printf("Logging setup error: %s\n", err)
   459  		os.Exit(1)
   460  		return
   461  	}
   462  	log.Info("logging initialized")
   463  }
   464  
   465  type options struct {
   466  	OptArgs                []string
   467  	OptCommand             string
   468  	OptDir                 string   `long:"dir" arg:"path" description:"working directory, start_server do chdir to before exec (optional)"`
   469  	OptInterval            int      `long:"interval" arg:"seconds" description:"minimum interval (in seconds) to respawn the server program (default: 1)"`
   470  	OptPorts               []string `long:"port" arg:"(port|host:port)" description:"TCP port to listen to (if omitted, will not bind to any ports)"`
   471  	OptPaths               []string `long:"path" arg:"path" description:"path at where to listen using unix socket (optional)"`
   472  	OptSignalOnHUP         string   `long:"signal-on-hup" arg:"Signal" description:"name of the signal to be sent to the server process when start_server\nreceives a SIGHUP (default: SIGTERM). If you use this option, be sure to\nalso use '--signal-on-term' below."`
   473  	OptSignalOnTERM        string   `long:"signal-on-term" arg:"Signal" description:"name of the signal to be sent to the server process when start_server\nreceives a SIGTERM (default: SIGTERM)"`
   474  	OptPidFile             string   `long:"pid-file" arg:"filename" description:"if set, writes the process id of the start_server process to the file"`
   475  	OptStatusFile          string   `long:"status-file" arg:"filename" description:"if set, writes the status of the server process(es) to the file"`
   476  	OptEnvdir              string   `long:"envdir" arg:"Envdir" description:"directory that contains environment variables to the server processes.\nIt is intended for use with \"envdir\" in \"daemontools\". This can be\noverwritten by environment variable \"ENVDIR\"."`
   477  	OptEnableAutoRestart   bool     `long:"enable-auto-restart" description:"enables automatic restart by time. This can be overwritten by\nenvironment variable \"ENABLE_AUTO_RESTART\"." note:"unimplemented"`
   478  	OptAutoRestartInterval int      `long:"auto-restart-interval" arg:"seconds" description:"automatic restart interval (default 360). It is used with\n\"--enable-auto-restart\" option. This can be overwritten by environment\nvariable \"AUTO_RESTART_INTERVAL\"." note:"unimplemented"`
   479  	OptKillOldDelay        int      `long:"kill-old-delay" arg:"seconds" description:"time to suspend to send a signal to the old worker. The default value is\n5 when \"--enable-auto-restart\" is set, 0 otherwise. This can be\noverwritten by environment variable \"KILL_OLD_DELAY\"."`
   480  	OptRestart             bool     `long:"restart" description:"this is a wrapper command that reads the pid of the start_server process\nfrom --pid-file, sends SIGHUP to the process and waits until the\nserver(s) of the older generation(s) die by monitoring the contents of\nthe --status-file" note:"unimplemented"`
   481  	OptHelp                bool     `long:"help" description:"prints this help"`
   482  	OptVersion             bool     `long:"version" description:"prints the version number"`
   483  }
   484  
   485  func (o options) Args() []string          { return o.OptArgs }
   486  func (o options) Command() string         { return o.OptCommand }
   487  func (o options) Dir() string             { return o.OptDir }
   488  func (o options) Interval() time.Duration { return time.Duration(o.OptInterval) * time.Second }
   489  func (o options) PidFile() string         { return o.OptPidFile }
   490  func (o options) Ports() []string         { return o.OptPorts }
   491  func (o options) Paths() []string         { return o.OptPaths }
   492  func (o options) SignalOnHUP() os.Signal  { return starter.SigFromName(o.OptSignalOnHUP) }
   493  func (o options) SignalOnTERM() os.Signal { return starter.SigFromName(o.OptSignalOnTERM) }
   494  func (o options) StatusFile() string      { return o.OptStatusFile }
   495  
   496  func getGraceServerCommand() cli.Command {
   497  	return cli.Command{
   498  		Name:        "glace-server",
   499  		ShortName:   "gsrv",
   500  		Usage:       "Run API Server with graceful restart support",
   501  		Description: "Run Gohan API server with graceful restart support",
   502  		Flags: []cli.Flag{
   503  			cli.StringFlag{Name: "config-file", Value: defaultConfigFile, Usage: "Server config File"},
   504  		},
   505  		Action: func(c *cli.Context) {
   506  			configFile := c.String("config-file")
   507  			loadConfig(configFile)
   508  			opts := &options{OptInterval: -1}
   509  			opts.OptCommand = os.Args[0]
   510  			config := util.GetConfig()
   511  			opts.OptPorts = []string{config.GetString("address", ":9091")}
   512  			opts.OptArgs = []string{"server", "--config-file", configFile}
   513  			s, err := starter.NewStarter(opts)
   514  			if err != nil {
   515  				fmt.Fprintf(os.Stderr, "error: %s\n", err)
   516  				return
   517  			}
   518  			s.Run()
   519  		},
   520  	}
   521  }