gitlab.com/ignitionrobotics/web/ign-go@v1.0.0-rc4/main.go (about) 1 package ign 2 3 // Import this file's dependencies 4 import ( 5 "errors" 6 "fmt" 7 "github.com/gorilla/mux" 8 "github.com/jinzhu/gorm" 9 "github.com/rollbar/rollbar-go" 10 "gitlab.com/ignitionrobotics/web/ign-go/monitoring" 11 "log" 12 "net/http" 13 "os" 14 "os/exec" 15 "path" 16 "runtime" 17 "strconv" 18 "strings" 19 "time" 20 21 // Needed by dbInit 22 _ "github.com/go-sql-driver/mysql" 23 ) 24 25 // Server encapsulates information needed by a downstream application 26 type Server struct { 27 /// Global database interface 28 Db *gorm.DB 29 30 /// Global database to the user database interface 31 UsersDb *gorm.DB 32 33 Router *mux.Router 34 35 // monitoring contains an optional monitoring provider used to 36 // keep track of server metrics. The metrics are defined by the provider. 37 // If set, the server will automatically add monitoring middleware when 38 // configuring routes and expose an endpoint to allow the monitoring system 39 // to scrape metric data. 40 monitoring monitoring.Provider 41 42 // Port used for non-secure requests 43 HTTPPort string 44 45 // SSLport used for secure requests 46 SSLport string 47 48 // SSLCert is the path to the SSL certificate. 49 SSLCert string 50 51 // SSLKey is the path to the SSL private key. 52 SSLKey string 53 54 // DbConfig contains information about the database 55 DbConfig DatabaseConfig 56 57 // IsTest is true when tests are running. 58 IsTest bool 59 60 /// Auth0 public key used for token validation 61 auth0RsaPublickey string 62 // PEM Key string generated from the auth0RsaPublickey value 63 pemKeyString string 64 65 // Google Analytics tracking ID. The format is UA-XXXX-Y 66 GaTrackingID string 67 68 // Google Analytics Application Name 69 GaAppName string 70 71 // (optional) A string to use as a prefix to GA Event Category. 72 GaCategoryPrefix string 73 74 // Should the Server log to stdout/err? Can be configured using IGN_LOGGER_LOG_STDOUT env var. 75 LogToStd bool 76 // Verbosity level of the Ign Logger - 4 debug, 3 info, 2 warning, 1 error, 0 critical 77 LogVerbosity int 78 // Verbosity level of the Ign Logger, to send to Rollbar - 4 debug, 3 info, 2 warning, 1 error, 0 critical 79 RollbarLogVerbosity int 80 } 81 82 // IsUsingSSL returns true if the server was configured to use SSL. 83 func (s *Server) IsUsingSSL() bool { 84 return s.SSLCert != "" && s.SSLKey != "" 85 } 86 87 // DatabaseConfig contains information about a database connection 88 type DatabaseConfig struct { 89 // Username to login to a database. 90 UserName string 91 // Password to login to a database. 92 Password string 93 // Address of the database. 94 Address string 95 // Name of the database. 96 Name string 97 // Allowed Max Open Connections. 98 // A value <= 0 means unlimited connections. 99 // See 'https://golang.org/src/database/sql/sql.go' 100 MaxOpenConns int 101 // True to enable database logging. This will cause all database transactions 102 // to output messages to standard out, which could create large log files in 103 // docker. It is recommended to use this only during development and testing. 104 // Logging can be controlled via the IGN_DB_LOG environment variable. 105 // 106 // By default logging is enabled only in tests with verbose flag. 107 EnableLog bool 108 } 109 110 // gServer is an internal pointer to the Server. 111 var gServer *Server 112 113 // Init initialize this package. 114 // Note: This method does not configure the Server's Router. You will later 115 // need to configure the router and set it to the server. 116 func Init(auth0RSAPublicKey string, dbNameSuffix string, monitoring monitoring.Provider) (server *Server, err error) { 117 118 // Configure and setup rollbar. 119 // Do this first so that the logging connection is established. 120 rollbarConfigure() 121 122 server = &Server{ 123 HTTPPort: ":8000", 124 SSLport: ":4430", 125 monitoring: monitoring, 126 } 127 server.readPropertiesFromEnvVars() 128 129 gServer = server 130 131 // Testing 132 server.IsTest = strings.Contains(strings.ToLower(os.Args[0]), "test") 133 134 if server.IsTest { 135 // Let's use a separate DB name if under test mode. 136 server.DbConfig.Name = server.DbConfig.Name + "_test" 137 server.DbConfig.EnableLog = true 138 } 139 140 server.DbConfig.Name = server.DbConfig.Name + dbNameSuffix 141 142 // Initialize the database 143 err = server.dbInit() 144 145 if err != nil { 146 log.Println(err) 147 } 148 149 if server.IsTest { 150 server.initTests() 151 } else { 152 server.SetAuth0RsaPublicKey(auth0RSAPublicKey) 153 } 154 155 return 156 } 157 158 // ConfigureRouterWithRoutes takes a given mux.Router and configures it with a set of 159 // declared routes. The router is configured with default middlewares. 160 // If a monitoring provider was set on the server, the router will include an additional middleware 161 // to track server metrics and add a monitoring route defined by the provider. 162 // If the router is a mux subrouter gotten by PathPrefix().Subrouter() then you need to 163 // pass the pathPrefix as argument here too (eg. "/2.0/") 164 func (s *Server) ConfigureRouterWithRoutes(pathPrefix string, router *mux.Router, routes Routes) { 165 rc := NewRouterConfigurer(router, s.monitoring) 166 rc.SetAuthHandlers( 167 CreateJWTOptionalMiddleware(s), 168 CreateJWTRequiredMiddleware(s), 169 ) 170 rc.ConfigureRouter(pathPrefix, routes) 171 } 172 173 // SetRouter sets the main mux.Router to the server. 174 // If a monitoring provider has been defined, this will also configure 175 // the router to include routes for the monitoring service. 176 func (s *Server) SetRouter(router *mux.Router) *Server { 177 if s.monitoring != nil { 178 subrouter := router.PathPrefix("/").Subrouter() 179 s.ConfigureRouterWithRoutes("/", subrouter, Routes{s.getMetricsRoute()}) 180 } 181 182 s.Router = router 183 return s 184 } 185 186 // ReadStdLogEnvVar reads the IGN_LOGGER_LOG_STDOUT env var and returns its bool value. 187 func ReadStdLogEnvVar() bool { 188 // Get whether to enable logging to stdout/err 189 strValue, err := ReadEnvVar("IGN_LOGGER_LOG_STDOUT") 190 if err != nil { 191 log.Printf("Error parsing IGN_LOGGER_LOG_STDOUT env variable. Disabling log to std.") 192 return false 193 } 194 195 flag, err := strconv.ParseBool(strValue) 196 if err != nil { 197 log.Printf("Error parsing IGN_LOGGER_LOG_STDOUT env variable. Disabling log to std.") 198 flag = false 199 } 200 return flag 201 } 202 203 // ReadLogVerbosityEnvVar reads the IGN_LOGGER_VERBOSITY env var and returns its bool value. 204 func ReadLogVerbosityEnvVar() int { 205 // Get whether to enable logging to stdout/err 206 strValue, err := ReadEnvVar("IGN_LOGGER_VERBOSITY") 207 if err != nil { 208 log.Printf("Error parsing IGN_LOGGER_VERBOSITY env variable. Using default values") 209 return VerbosityWarning 210 } 211 212 val, err := strconv.ParseInt(strValue, 10, 32) 213 if err != nil { 214 log.Printf("Error parsing IGN_LOGGER_VERBOSITY env variable. Using default values") 215 return VerbosityWarning 216 } 217 218 return int(val) 219 } 220 221 // ReadRollbarLogVerbosityEnvVar reads the IGN_LOGGER_ROLLBAR_VERBOSITY env var and returns its bool value. 222 func ReadRollbarLogVerbosityEnvVar() int { 223 rollbarVerbStr, err := ReadEnvVar("IGN_LOGGER_ROLLBAR_VERBOSITY") 224 if err != nil { 225 log.Printf("Error parsing IGN_LOGGER_ROLLBAR_VERBOSITY env variable. Using WARNING as default value") 226 return VerbosityWarning 227 } 228 229 val, err := strconv.ParseInt(rollbarVerbStr, 10, 32) 230 if err != nil { 231 log.Printf("Error parsing IGN_LOGGER_ROLLBAR_VERBOSITY env variable. Using WARNING as default value") 232 return VerbosityWarning 233 } 234 return int(val) 235 } 236 237 // NewDatabaseConfigFromEnvVars returns a DatabaseConfig object from the following env vars: 238 // - IGN_DB_USERNAME 239 // - IGN_DB_PASSWORD 240 // - IGN_DB_ADDRESS 241 // - IGN_DB_NAME 242 // - IGN_DB_MAX_OPEN_CONNS - (Optional) You run the risk of getting a 'too many connections' error if this is not set. 243 func NewDatabaseConfigFromEnvVars() (DatabaseConfig, error) { 244 dbConfig := DatabaseConfig{} 245 var err error 246 247 // Get the database username 248 if dbConfig.UserName, err = ReadEnvVar("IGN_DB_USERNAME"); err != nil { 249 errMsg := "Missing IGN_DB_USERNAME env variable. Database connection will not work" 250 return dbConfig, errors.New(errMsg) 251 } 252 253 // Get the database password 254 if dbConfig.Password, err = ReadEnvVar("IGN_DB_PASSWORD"); err != nil { 255 errMsg := "Missing IGN_DB_PASSWORD env variable. Database connection will not work" 256 return dbConfig, errors.New(errMsg) 257 } 258 259 // Get the database address 260 if dbConfig.Address, err = ReadEnvVar("IGN_DB_ADDRESS"); err != nil { 261 errMsg := "Missing IGN_DB_ADDRESS env variable. Database connection will not work" 262 return dbConfig, errors.New(errMsg) 263 } 264 265 // Get the database name 266 if dbConfig.Name, err = ReadEnvVar("IGN_DB_NAME"); err != nil { 267 errMsg := "Missing IGN_DB_NAME env variable. Database connection will not work" 268 return dbConfig, errors.New(errMsg) 269 } 270 271 // Get the database max open conns 272 var maxStr string 273 if maxStr, err = ReadEnvVar("IGN_DB_MAX_OPEN_CONNS"); err != nil { 274 log.Printf("Missing IGN_DB_MAX_OPEN_CONNS env variable." + 275 "Database max open connections will be set to unlimited," + 276 "with the risk of getting 'too many connections' error.") 277 dbConfig.MaxOpenConns = 0 278 } else { 279 var i int64 280 i, err = strconv.ParseInt(maxStr, 10, 32) 281 if err != nil || i <= 0 { 282 log.Printf("Error parsing IGN_DB_MAX_OPEN_CONNS env variable." + 283 "Database max open connections will be set to unlimited," + 284 "with the risk of getting 'too many connections' error.") 285 dbConfig.MaxOpenConns = 0 286 } else { 287 dbConfig.MaxOpenConns = int(i) 288 } 289 } 290 291 return dbConfig, nil 292 } 293 294 // readPropertiesFromEnvVars configures the server based on env vars. 295 func (s *Server) readPropertiesFromEnvVars() error { 296 var err error 297 298 // Get the HTTP Port, if specified. 299 if httpPort, err := ReadEnvVar("IGN_HTTP_PORT"); err != nil { 300 log.Printf("Missing IGN_HTTP_PORT env variable. Server will use %s.\n", s.HTTPPort) 301 } else { 302 s.HTTPPort = httpPort 303 } 304 305 // Get the SSL Port, if specified. 306 if sslPort, err := ReadEnvVar("IGN_SSL_PORT"); err != nil { 307 log.Printf("Missing IGN_SSL_PORT env variable. Server will use %s.", s.SSLport) 308 } else { 309 s.SSLport = sslPort 310 } 311 312 // Get the SSL certificate, if specified. 313 if s.SSLCert, err = ReadEnvVar("IGN_SSL_CERT"); err != nil { 314 log.Printf("Missing IGN_SSL_CERT env variable. " + 315 "Server will not be secure (no https).") 316 } 317 // Get the SSL private key, if specified. 318 if s.SSLKey, err = ReadEnvVar("IGN_SSL_KEY"); err != nil { 319 log.Printf("Missing IGN_SSL_KEY env variable. " + 320 "Server will not be secure (no https).") 321 } 322 323 // Read Google Analytics parameters 324 if s.GaTrackingID, err = ReadEnvVar("IGN_GA_TRACKING_ID"); err != nil { 325 log.Printf("Missing IGN_GA_TRACKING_ID env variable. GA will not be enabled") 326 } 327 if s.GaAppName, err = ReadEnvVar("IGN_GA_APP_NAME"); err != nil { 328 log.Printf("Missing IGN_GA_APP_NAME env variable. GA will not be enabled") 329 } 330 if s.GaCategoryPrefix, err = ReadEnvVar("IGN_GA_CAT_PREFIX"); err != nil { 331 log.Printf("Missing optional IGN_GA_CAT_PREFIX env variable.") 332 } 333 334 if s.DbConfig, err = NewDatabaseConfigFromEnvVars(); err != nil { 335 log.Printf(err.Error()) 336 } 337 338 // Get whether to enable database logging 339 var dbLogStr string 340 s.DbConfig.EnableLog = false 341 if dbLogStr, err = ReadEnvVar("IGN_DB_LOG"); err == nil { 342 if s.DbConfig.EnableLog, err = strconv.ParseBool(dbLogStr); err != nil { 343 log.Printf("Error parsing IGN_DB_LOG env variable." + 344 "Database logging will be disabled.") 345 } 346 } 347 348 // Get whether to enable logging to stdout/err and the verbosity level. 349 s.LogToStd = ReadStdLogEnvVar() 350 s.LogVerbosity = ReadLogVerbosityEnvVar() 351 s.RollbarLogVerbosity = ReadRollbarLogVerbosityEnvVar() 352 353 return nil 354 } 355 356 // Auth0RsaPublicKey return the Auth0 public key 357 func (s *Server) Auth0RsaPublicKey() string { 358 return s.auth0RsaPublickey 359 } 360 361 // SetAuth0RsaPublicKey sets the server's Auth0 RSA public key 362 func (s *Server) SetAuth0RsaPublicKey(key string) { 363 s.auth0RsaPublickey = key 364 s.pemKeyString = "-----BEGIN CERTIFICATE-----\n" + s.auth0RsaPublickey + 365 "\n-----END CERTIFICATE-----" 366 } 367 368 // Run the router and server 369 func (s *Server) Run() { 370 371 if s.SSLCert != "" && s.SSLKey != "" { 372 // Start the webserver with TLS support. 373 log.Fatal(http.ListenAndServeTLS(s.SSLport, s.SSLCert, s.SSLKey, s.Router)) 374 } else { 375 // Start the http webserver 376 log.Fatal(http.ListenAndServe(s.HTTPPort, s.Router)) 377 } 378 379 // Wait for all rollbar messages to complete 380 rollbar.Wait() 381 } 382 383 ///////////////////////////////////////////////// 384 // Private functions 385 386 // initTests is run as the last step of init() and only when `go test` was run. 387 func (s *Server) initTests() { 388 // Override Auth0 public RSA key with test key, if present 389 if testKey, err := ReadEnvVar("TEST_RSA256_PUBLIC_KEY"); err != nil { 390 log.Printf("Missing TEST_RSA256_PUBLIC_KEY. Test with authentication may not work.") 391 } else { 392 s.SetAuth0RsaPublicKey(testKey) 393 } 394 } 395 396 // connectDb is called to establish a db connection. The target database is created if it doesn't exist. 397 func connectDb(driver, url string, cfg *DatabaseConfig) (db *gorm.DB, err error) { 398 399 // Queries 400 queryCreate := fmt.Sprintf("CREATE DATABASE %s", cfg.Name) 401 queryUse := fmt.Sprintf("USE %s", cfg.Name) 402 403 // Error message 404 errUseDb := fmt.Sprintf("[ERROR] Unable to use the %s database", cfg.Name) 405 406 // Open db connection 407 db, err = gorm.Open(driver, url) 408 409 if err != nil { 410 return nil, errors.New("[ERROR] Unable to connect to the database system") 411 } 412 413 // Execute db creation 414 _, err = db.DB().Exec(queryCreate) 415 416 // If the step before does not throw any errors, it means that the database was successfully created 417 // In the other hand, if it throws an error, it means that the database already exists 418 if err == nil { 419 log.Printf("[SUCCESS] Database %s was successfully created. Trying to connect once again...\n", cfg.Name) 420 } 421 422 // We have ensured that the database was created, let's use it. 423 _, err = db.DB().Exec(queryUse) 424 425 // If there was an error, it means that the database is not available 426 if err != nil { 427 return nil, errors.New(errUseDb) 428 } 429 430 // Close and reopen the DB to the correct database. 431 db.Close() 432 url = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=UTC", 433 cfg.UserName, cfg.Password, cfg.Address, cfg.Name) 434 db, _ = gorm.Open(driver, url) 435 436 return db, err 437 } 438 439 // InitDbWithCfg initialize the database connection based on the given cfg. 440 func InitDbWithCfg(cfg *DatabaseConfig) (*gorm.DB, error) { 441 // Connect to the database 442 url := fmt.Sprintf("%s:%s@tcp(%s)/?charset=utf8&parseTime=True&loc=UTC", 443 cfg.UserName, cfg.Password, cfg.Address) 444 445 var err error 446 var db *gorm.DB 447 448 for i := 0; i < 10; i++ { 449 db, err = connectDb("mysql", url, cfg) 450 451 if err == nil { 452 break 453 } 454 455 log.Printf("Attempt[%d] to connect to the database failed.\n", i) 456 log.Println(url) 457 log.Println(err) 458 time.Sleep(5) 459 } 460 461 return db, err 462 } 463 464 // dbInit Initialize the database connection 465 func (s *Server) dbInit() error { 466 var err error 467 s.Db, err = InitDbWithCfg(&s.DbConfig) 468 // By default, assume this database is the User Database 469 // Note: web-cloudsim uses a different default Db, and sets the UsersDb 470 // appropriately in order to support access tokens. 471 // \todo(anyone) Fix/remove this when the User database is moved to its own 472 // server/service. 473 s.UsersDb = s.Db 474 return err 475 } 476 477 // rollbarConfigure setups up the rollbar connection. 478 func rollbarConfigure() { 479 // token is the rollbar connection token. 480 var token string 481 482 // env is the environment string, usually "staging" or "production" 483 var env string 484 485 // Path to the application code root, not including the final slash. 486 // Used to collapse non-project code when displaying tracebacks. 487 var root string 488 489 var err error 490 491 // Get the rollbar token 492 if token, err = ReadEnvVar("IGN_ROLLBAR_TOKEN"); err != nil { 493 log.Printf("Missing IGN_ROLLBAR_TOKEN env variable." + 494 "Rollbar connection will not work.") 495 // Short circuit. 496 return 497 } 498 499 // Get the rollbar environment 500 if env, err = ReadEnvVar("IGN_ROLLBAR_ENV"); err != nil { 501 log.Printf("Missing IGN_ROLLBAR_ENV env variable." + 502 "Rollbar environment will be development.") 503 env = "development" 504 } 505 506 // Get the rollbar root 507 if root, err = ReadEnvVar("IGN_ROLLBAR_ROOT"); err != nil { 508 log.Printf("Missing IGN_ROLLBAR_ROOT env variable." + 509 "Rollbar will use bitbucket.org/ignitionrobotics.") 510 root = "bitbucket.org/ignitionrobotics" 511 } 512 513 // Configure rollbar token. 514 rollbar.SetToken(token) 515 516 // Configure rollbar environment. 517 rollbar.SetEnvironment(env) 518 519 // Configure rollbar server root 520 rollbar.SetServerRoot(root) 521 522 // CodeVersion is a string, up to 40 characters, describing the version of 523 // the application code. Rollbar understands these formats: 524 // - semantic version (i.e. "2.1.12") 525 // - integer (i.e. "45") 526 // - SHA (i.e. "3da541559918a808c2402bba5012f6c60b27661c") 527 // 528 // We automatically acquire the version using mercurial 529 rollbar.SetCodeVersion("unknown") 530 _, filename, _, _ := runtime.Caller(1) 531 if codeVersion, err := exec.Command("hg", "id", "-i", path.Dir(filename)).Output(); err == nil { 532 rollbar.SetCodeVersion(string(codeVersion[:])) 533 } 534 535 // Set the server hostname 536 if hostname, err := os.Hostname(); err == nil { 537 rollbar.SetServerHost(hostname) 538 } else { 539 rollbar.SetServerHost("error reading hostname") 540 } 541 } 542 543 // generateMetricsRoute is an internal method to generate a metrics route. 544 // This route is called by the monitoring system to scrape server metric data. 545 func (s *Server) getMetricsRoute() Route { 546 return Route{ 547 Name: "Metrics", 548 Description: "Provides server metrics for monitoring systems.", 549 URI: s.monitoring.MetricsRoute(), 550 Methods: Methods{ 551 Method{ 552 Type: "GET", 553 Description: "Get server metrics.", 554 Handlers: FormatHandlers{ 555 FormatHandler{ 556 Extension: "", 557 Handler: s.monitoring.MetricsHandler(), 558 }, 559 }, 560 }, 561 }, 562 } 563 }