2020-12-02 10:09:37 -05:00
package main
import (
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
_ "net/http/pprof"
"os"
"syscall"
"time"
"gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/labkit/monitoring"
"gitlab.com/gitlab-org/labkit/tracing"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/queueing"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/redis"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream"
)
// Version is the current version of GitLab Workhorse
var Version = "(unknown version)" // Set at build time in the Makefile
// BuildTime signifies the time the binary was build
var BuildTime = "19700101.000000" // Set at build time in the Makefile
type bootConfig struct {
secretPath string
listenAddr string
listenNetwork string
listenUmask int
pprofListenAddr string
prometheusListenAddr string
logFile string
logFormat string
printVersion bool
}
func main ( ) {
boot , cfg , err := buildConfig ( os . Args [ 0 ] , os . Args [ 1 : ] )
if err == ( alreadyPrintedError { flag . ErrHelp } ) {
os . Exit ( 0 )
}
if err != nil {
if _ , alreadyPrinted := err . ( alreadyPrintedError ) ; ! alreadyPrinted {
fmt . Fprintln ( os . Stderr , err )
}
os . Exit ( 2 )
}
if boot . printVersion {
fmt . Printf ( "gitlab-workhorse %s-%s\n" , Version , BuildTime )
os . Exit ( 0 )
}
log . WithError ( run ( * boot , * cfg ) ) . Fatal ( "shutting down" )
}
type alreadyPrintedError struct { error }
// buildConfig may print messages to os.Stderr if err != nil. If err is
// of type alreadyPrintedError it has already been printed.
func buildConfig ( arg0 string , args [ ] string ) ( * bootConfig , * config . Config , error ) {
boot := & bootConfig { }
cfg := & config . Config { Version : Version }
fset := flag . NewFlagSet ( arg0 , flag . ContinueOnError )
fset . Usage = func ( ) {
fmt . Fprintf ( fset . Output ( ) , "Usage of %s:\n" , arg0 )
fmt . Fprintf ( fset . Output ( ) , "\n %s [OPTIONS]\n\nOptions:\n" , arg0 )
fset . PrintDefaults ( )
}
configFile := fset . String ( "config" , "" , "TOML file to load config from" )
fset . StringVar ( & boot . secretPath , "secretPath" , "./.gitlab_workhorse_secret" , "File with secret key to authenticate with authBackend" )
fset . StringVar ( & boot . listenAddr , "listenAddr" , "localhost:8181" , "Listen address for HTTP server" )
fset . StringVar ( & boot . listenNetwork , "listenNetwork" , "tcp" , "Listen 'network' (tcp, tcp4, tcp6, unix)" )
fset . IntVar ( & boot . listenUmask , "listenUmask" , 0 , "Umask for Unix socket" )
fset . StringVar ( & boot . pprofListenAddr , "pprofListenAddr" , "" , "pprof listening address, e.g. 'localhost:6060'" )
fset . StringVar ( & boot . prometheusListenAddr , "prometheusListenAddr" , "" , "Prometheus listening address, e.g. 'localhost:9229'" )
fset . StringVar ( & boot . logFile , "logFile" , "" , "Log file location" )
fset . StringVar ( & boot . logFormat , "logFormat" , "text" , "Log format to use defaults to text (text, json, structured, none)" )
fset . BoolVar ( & boot . printVersion , "version" , false , "Print version and exit" )
// gitlab-rails backend
authBackend := fset . String ( "authBackend" , upstream . DefaultBackend . String ( ) , "Authentication/authorization backend" )
fset . StringVar ( & cfg . Socket , "authSocket" , "" , "Optional: Unix domain socket to dial authBackend at" )
// actioncable backend
cableBackend := fset . String ( "cableBackend" , upstream . DefaultBackend . String ( ) , "ActionCable backend" )
fset . StringVar ( & cfg . CableSocket , "cableSocket" , "" , "Optional: Unix domain socket to dial cableBackend at" )
fset . StringVar ( & cfg . DocumentRoot , "documentRoot" , "public" , "Path to static files content" )
fset . DurationVar ( & cfg . ProxyHeadersTimeout , "proxyHeadersTimeout" , 5 * time . Minute , "How long to wait for response headers when proxying the request" )
fset . BoolVar ( & cfg . DevelopmentMode , "developmentMode" , false , "Allow the assets to be served from Rails app" )
fset . UintVar ( & cfg . APILimit , "apiLimit" , 0 , "Number of API requests allowed at single time" )
fset . UintVar ( & cfg . APIQueueLimit , "apiQueueLimit" , 0 , "Number of API requests allowed to be queued" )
fset . DurationVar ( & cfg . APIQueueTimeout , "apiQueueDuration" , queueing . DefaultTimeout , "Maximum queueing duration of requests" )
fset . DurationVar ( & cfg . APICILongPollingDuration , "apiCiLongPollingDuration" , 50 , "Long polling duration for job requesting for runners (default 50s - enabled)" )
fset . BoolVar ( & cfg . PropagateCorrelationID , "propagateCorrelationID" , false , "Reuse existing Correlation-ID from the incoming request header `X-Request-ID` if present" )
if err := fset . Parse ( args ) ; err != nil {
return nil , nil , alreadyPrintedError { err }
}
if fset . NArg ( ) > 0 {
err := alreadyPrintedError { fmt . Errorf ( "unexpected arguments: %v" , fset . Args ( ) ) }
fmt . Fprintln ( fset . Output ( ) , err )
fset . Usage ( )
return nil , nil , err
}
var err error
cfg . Backend , err = parseAuthBackend ( * authBackend )
if err != nil {
return nil , nil , fmt . Errorf ( "authBackend: %v" , err )
}
cfg . CableBackend , err = parseAuthBackend ( * cableBackend )
if err != nil {
return nil , nil , fmt . Errorf ( "cableBackend: %v" , err )
}
tomlData := ""
if * configFile != "" {
buf , err := ioutil . ReadFile ( * configFile )
if err != nil {
return nil , nil , fmt . Errorf ( "configFile: %v" , err )
}
tomlData = string ( buf )
}
cfgFromFile , err := config . LoadConfig ( tomlData )
if err != nil {
return nil , nil , fmt . Errorf ( "configFile: %v" , err )
}
cfg . Redis = cfgFromFile . Redis
cfg . ObjectStorageCredentials = cfgFromFile . ObjectStorageCredentials
cfg . ImageResizerConfig = cfgFromFile . ImageResizerConfig
2020-12-08 22:09:24 -05:00
cfg . AltDocumentRoot = cfgFromFile . AltDocumentRoot
2020-12-02 10:09:37 -05:00
return boot , cfg , nil
}
// run() lets us use normal Go error handling; there is no log.Fatal in run().
func run ( boot bootConfig , cfg config . Config ) error {
closer , err := startLogging ( boot . logFile , boot . logFormat )
if err != nil {
return err
}
defer closer . Close ( )
tracing . Initialize ( tracing . WithServiceName ( "gitlab-workhorse" ) )
log . WithField ( "version" , Version ) . WithField ( "build_time" , BuildTime ) . Print ( "Starting" )
// Good housekeeping for Unix sockets: unlink before binding
if boot . listenNetwork == "unix" {
if err := os . Remove ( boot . listenAddr ) ; err != nil && ! os . IsNotExist ( err ) {
return err
}
}
// Change the umask only around net.Listen()
oldUmask := syscall . Umask ( boot . listenUmask )
listener , err := net . Listen ( boot . listenNetwork , boot . listenAddr )
syscall . Umask ( oldUmask )
if err != nil {
return fmt . Errorf ( "main listener: %v" , err )
}
finalErrors := make ( chan error )
// The profiler will only be activated by HTTP requests. HTTP
// requests can only reach the profiler if we start a listener. So by
// having no profiler HTTP listener by default, the profiler is
// effectively disabled by default.
if boot . pprofListenAddr != "" {
l , err := net . Listen ( "tcp" , boot . pprofListenAddr )
if err != nil {
return fmt . Errorf ( "pprofListenAddr: %v" , err )
}
go func ( ) { finalErrors <- http . Serve ( l , nil ) } ( )
}
monitoringOpts := [ ] monitoring . Option { monitoring . WithBuildInformation ( Version , BuildTime ) }
if boot . prometheusListenAddr != "" {
l , err := net . Listen ( "tcp" , boot . prometheusListenAddr )
if err != nil {
return fmt . Errorf ( "prometheusListenAddr: %v" , err )
}
monitoringOpts = append ( monitoringOpts , monitoring . WithListener ( l ) )
}
go func ( ) {
// Unlike http.Serve, which always returns a non-nil error,
// monitoring.Start may return nil in which case we should not shut down.
if err := monitoring . Start ( monitoringOpts ... ) ; err != nil {
finalErrors <- err
}
} ( )
secret . SetPath ( boot . secretPath )
if cfg . Redis != nil {
redis . Configure ( cfg . Redis , redis . DefaultDialFunc )
go redis . Process ( )
}
if err := cfg . RegisterGoCloudURLOpeners ( ) ; err != nil {
return fmt . Errorf ( "register cloud credentials: %v" , err )
}
accessLogger , accessCloser , err := getAccessLogger ( boot . logFile , boot . logFormat )
if err != nil {
return fmt . Errorf ( "configure access logger: %v" , err )
}
defer accessCloser . Close ( )
up := wrapRaven ( upstream . NewUpstream ( cfg , accessLogger ) )
go func ( ) { finalErrors <- http . Serve ( listener , up ) } ( )
return <- finalErrors
}