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 }