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  }