github.com/imyousuf/webhook-broker@v0.1.2/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  	"syscall"
    14  	"time"
    15  
    16  	"github.com/rs/zerolog"
    17  	"github.com/rs/zerolog/log"
    18  
    19  	"github.com/google/wire"
    20  	"github.com/imyousuf/webhook-broker/config"
    21  	"github.com/imyousuf/webhook-broker/controllers"
    22  	"github.com/imyousuf/webhook-broker/dispatcher"
    23  	"github.com/imyousuf/webhook-broker/storage"
    24  	"github.com/imyousuf/webhook-broker/storage/data"
    25  	lumberjack "gopkg.in/natefinch/lumberjack.v2"
    26  )
    27  
    28  // ServerLifecycleListenerImpl is a blocking implementation around main method to wait on server actions
    29  type ServerLifecycleListenerImpl struct {
    30  	shutdownListener chan bool
    31  }
    32  
    33  // StartingServer called when listening is being started
    34  func (impl *ServerLifecycleListenerImpl) StartingServer() {}
    35  
    36  // ServerStartFailed called when server start failed due to error
    37  func (impl *ServerLifecycleListenerImpl) ServerStartFailed(err error) {}
    38  
    39  // ServerShutdownCompleted called once server has been shutdown
    40  func (impl *ServerLifecycleListenerImpl) ServerShutdownCompleted() {
    41  	go func() {
    42  		impl.shutdownListener <- true
    43  	}()
    44  }
    45  
    46  // HTTPServiceContainer wrapper for IoC too return
    47  type HTTPServiceContainer struct {
    48  	Configuration *config.Config
    49  	Server        *http.Server
    50  	DataAccessor  storage.DataAccessor
    51  	Listener      *ServerLifecycleListenerImpl
    52  	Dispatcher    dispatcher.MessageDispatcher
    53  }
    54  
    55  var (
    56  	exit = func(code int) {
    57  		os.Exit(code)
    58  	}
    59  	consolePrintln = func(output string) {
    60  		fmt.Println(output)
    61  	}
    62  
    63  	// ErrMigrationSrcNotDir for error when migration source specified is not a directory
    64  	ErrMigrationSrcNotDir = errors.New("migration source not a dir")
    65  
    66  	parseArgs = func(programName string, args []string) (cliConfig *config.CLIConfig, output string, err error) {
    67  		flags := flag.NewFlagSet(programName, flag.ContinueOnError)
    68  		var buf bytes.Buffer
    69  		flags.SetOutput(&buf)
    70  
    71  		var conf config.CLIConfig
    72  		flags.StringVar(&conf.ConfigPath, "config", "", "Config file location")
    73  		flags.StringVar(&conf.MigrationSource, "migrate", "", "Migration source folder")
    74  		flags.BoolVar(&conf.StopOnConfigChange, "stop-on-conf-change", false, "Restart internally on -config change if this flag is absent")
    75  		flags.BoolVar(&conf.DoNotWatchConfigChange, "do-not-watch-conf-change", false, "Do not watch config change")
    76  
    77  		err = flags.Parse(args)
    78  		if err != nil {
    79  			return nil, buf.String(), err
    80  		}
    81  
    82  		if len(conf.MigrationSource) > 0 {
    83  			fileInfo, err := os.Stat(conf.MigrationSource)
    84  			if err != nil {
    85  				return nil, "Could not determine migration source details", err
    86  			}
    87  			if !fileInfo.IsDir() {
    88  				return nil, "Migration source must be a dir", ErrMigrationSrcNotDir
    89  			}
    90  			if !filepath.IsAbs(conf.MigrationSource) {
    91  				conf.MigrationSource, _ = filepath.Abs(conf.MigrationSource)
    92  			}
    93  			conf.MigrationSource = "file://" + conf.MigrationSource
    94  		}
    95  
    96  		return &conf, buf.String(), nil
    97  	}
    98  
    99  	getApp = func(httpServiceContainer *HTTPServiceContainer) (*data.App, error) {
   100  		return httpServiceContainer.DataAccessor.GetAppRepository().GetApp()
   101  	}
   102  
   103  	startAppInit = func(httpServiceContainer *HTTPServiceContainer, seedData *config.SeedData) error {
   104  		return httpServiceContainer.DataAccessor.GetAppRepository().StartAppInit(seedData)
   105  	}
   106  
   107  	getTimeoutTimer = func() <-chan time.Time {
   108  		return time.After(time.Second * 10)
   109  	}
   110  
   111  	waitDuration = 1 * time.Second
   112  
   113  	initApp = func(httpServiceContainer *HTTPServiceContainer) {
   114  		app, err := getApp(httpServiceContainer)
   115  		var initFinished chan bool = make(chan bool)
   116  		timeout := getTimeoutTimer()
   117  		if err == nil && app.GetStatus() == data.NotInitialized || (app.GetStatus() == data.Initialized && app.GetSeedData().DataHash != httpServiceContainer.Configuration.GetSeedData().DataHash) {
   118  			go func() {
   119  				run := true
   120  				for run {
   121  					select {
   122  					case <-timeout:
   123  						initFinished <- true
   124  						run = false
   125  					default:
   126  						seedData := httpServiceContainer.Configuration.GetSeedData()
   127  						initErr := startAppInit(httpServiceContainer, &seedData)
   128  						switch initErr {
   129  						case nil:
   130  							createSeedData(httpServiceContainer.DataAccessor, httpServiceContainer.Configuration)
   131  							completeErr := httpServiceContainer.DataAccessor.GetAppRepository().CompleteAppInit()
   132  							log.Error().Err(completeErr).Msg("init error in setting complete flag")
   133  							run = false
   134  						case storage.ErrAppInitializing:
   135  							run = false
   136  						case storage.ErrOptimisticAppInit:
   137  							run = false
   138  						default:
   139  							log.Error().Err(initErr).Msg("unexpected init error")
   140  							time.Sleep(waitDuration)
   141  						}
   142  					}
   143  				}
   144  				initFinished <- true
   145  			}()
   146  			<-initFinished
   147  		}
   148  	}
   149  
   150  	createSeedData = func(dataAccessor storage.DataAccessor, seedDataConfig config.SeedDataConfig) {
   151  		for _, seedProducer := range seedDataConfig.GetSeedData().Producers {
   152  			producer, err := data.NewProducer(seedProducer.ID, seedProducer.Token)
   153  			if err == nil {
   154  				producer.Name = seedProducer.Name
   155  				_, err = dataAccessor.GetProducerRepository().Store(producer)
   156  			}
   157  			if err != nil {
   158  				log.Error().Err(err).Msg("Error creating producer: " + seedProducer.ID)
   159  			}
   160  		}
   161  		for _, seedChannel := range seedDataConfig.GetSeedData().Channels {
   162  			channel, err := data.NewChannel(seedChannel.ID, seedChannel.Token)
   163  			if err == nil {
   164  				channel.Name = seedChannel.Name
   165  				_, err = dataAccessor.GetChannelRepository().Store(channel)
   166  			}
   167  			if err != nil {
   168  				log.Error().Err(err).Msg("Error creating channel" + seedChannel.ID)
   169  			}
   170  		}
   171  		for _, seedConsumer := range seedDataConfig.GetSeedData().Consumers {
   172  			channel, err := dataAccessor.GetChannelRepository().Get(seedConsumer.Channel)
   173  			if err != nil {
   174  				log.Error().Err(err).Msg("no channel for the consumer as per spec")
   175  				continue
   176  			}
   177  			consumer, err := data.NewConsumer(channel, seedConsumer.ID, seedConsumer.Token, seedConsumer.CallbackURL)
   178  			if err == nil {
   179  				consumer.Name = seedConsumer.Name
   180  				consumer.ConsumingFrom = channel
   181  				consumer.CallbackURL = seedConsumer.CallbackURL.String()
   182  				_, err = dataAccessor.GetConsumerRepository().Store(consumer)
   183  			}
   184  			if err != nil {
   185  				log.Error().Err(err).Msg("Error creating consumer" + seedConsumer.ID)
   186  			}
   187  		}
   188  	}
   189  )
   190  
   191  func main() {
   192  	log.Print("Webhook Broker - " + string(GetAppVersion()))
   193  	inConfig, output, cliCfgErr := parseArgs(os.Args[0], os.Args[1:])
   194  	if cliCfgErr != nil {
   195  		consolePrintln(output)
   196  		if cliCfgErr != flag.ErrHelp {
   197  			log.Error().Err(cliCfgErr).Msg("CLI config error")
   198  		}
   199  		exit(1)
   200  	}
   201  	log.Print("Configuration File (optional): " + inConfig.ConfigPath)
   202  	hasConfigChange := true
   203  	var mutex sync.Mutex
   204  	var setHasConfigChange = func(newVal bool) {
   205  		mutex.Lock()
   206  		defer mutex.Unlock()
   207  		hasConfigChange = newVal
   208  	}
   209  	log.Print("On config change will stop? - ", inConfig.StopOnConfigChange)
   210  	pid := syscall.Getpid()
   211  	inConfig.NotifyOnConfigFileChange(func() {
   212  		log.Print("Config file changed")
   213  		if !inConfig.StopOnConfigChange {
   214  			log.Print("Restarting")
   215  			setHasConfigChange(true)
   216  		}
   217  		syscall.Kill(pid, syscall.SIGINT)
   218  	})
   219  	for hasConfigChange {
   220  		setHasConfigChange(false)
   221  		// Setup HTTP Server and listen (implicitly init DB and run migration if arg passed)
   222  		httpServiceContainer, err := GetHTTPServer(inConfig)
   223  		if err != nil {
   224  			log.Error().Err(err).Msg("could not start http service")
   225  			exit(3)
   226  		}
   227  		_, err = getApp(httpServiceContainer)
   228  		if err == nil {
   229  			initApp(httpServiceContainer)
   230  		} else {
   231  			log.Error().Err(err).Msg("could not retrieve app to initialize")
   232  			exit(4)
   233  		}
   234  		var buf bytes.Buffer
   235  		json.NewEncoder(&buf).Encode(httpServiceContainer.Configuration)
   236  		log.Print("Configuration in Use : " + buf.String())
   237  		// Setup Log Output
   238  		setupLogger(httpServiceContainer.Configuration)
   239  		<-httpServiceContainer.Listener.shutdownListener
   240  		httpServiceContainer.Dispatcher.Stop()
   241  	}
   242  	inConfig.StopWatcher()
   243  }
   244  
   245  func setupLogger(logConfig config.LogConfig) {
   246  	switch logConfig.GetLogLevel() {
   247  	case config.Debug:
   248  		zerolog.SetGlobalLevel(zerolog.DebugLevel)
   249  	case config.Info:
   250  		zerolog.SetGlobalLevel(zerolog.InfoLevel)
   251  	case config.Error:
   252  		zerolog.SetGlobalLevel(zerolog.ErrorLevel)
   253  	case config.Fatal:
   254  		zerolog.SetGlobalLevel(zerolog.FatalLevel)
   255  	}
   256  	if logConfig.IsLoggerConfigAvailable() {
   257  		log.Logger = log.Output(&lumberjack.Logger{
   258  			Filename:   logConfig.GetLogFilename(),
   259  			MaxSize:    int(logConfig.GetMaxLogFileSize()), // megabytes
   260  			MaxBackups: int(logConfig.GetMaxLogBackups()),
   261  			MaxAge:     int(logConfig.GetMaxAgeForALogFile()),        //days
   262  			Compress:   logConfig.IsCompressionEnabledOnLogBackups(), // disabled by default
   263  		})
   264  	}
   265  }
   266  
   267  // Providers & Injectors
   268  
   269  // NewServerListener initializes new server listener
   270  func NewServerListener() *ServerLifecycleListenerImpl {
   271  	return &ServerLifecycleListenerImpl{shutdownListener: make(chan bool)}
   272  }
   273  
   274  // GetMigrationConfig is provider for migration config
   275  func GetMigrationConfig(cliConfig *config.CLIConfig) *storage.MigrationConfig {
   276  	return &storage.MigrationConfig{MigrationEnabled: cliConfig.IsMigrationEnabled(), MigrationSource: cliConfig.MigrationSource}
   277  }
   278  
   279  func newAppRepository(dataAccessor storage.DataAccessor) storage.AppRepository {
   280  	return dataAccessor.GetAppRepository()
   281  }
   282  
   283  func newProducerRepository(dataAccessor storage.DataAccessor) storage.ProducerRepository {
   284  	return dataAccessor.GetProducerRepository()
   285  }
   286  
   287  func newChannelRepository(dataAccessor storage.DataAccessor) storage.ChannelRepository {
   288  	return dataAccessor.GetChannelRepository()
   289  }
   290  
   291  func newConsumerRepository(dataAccessor storage.DataAccessor) storage.ConsumerRepository {
   292  	return dataAccessor.GetConsumerRepository()
   293  }
   294  
   295  func newMessageRepository(dataAccessor storage.DataAccessor) storage.MessageRepository {
   296  	return dataAccessor.GetMessageRepository()
   297  }
   298  
   299  func newDeliveryJobRepository(dataAccessor storage.DataAccessor) storage.DeliveryJobRepository {
   300  	return dataAccessor.GetDeliveryJobRepository()
   301  }
   302  
   303  func newLockRepository(dataAccessor storage.DataAccessor) storage.LockRepository {
   304  	return dataAccessor.GetLockRepository()
   305  }
   306  
   307  var (
   308  	httpServiceContainerInjectorSet = wire.NewSet(wire.Struct(new(HTTPServiceContainer), "Configuration", "Server", "DataAccessor", "Listener", "Dispatcher"))
   309  	configInjectorSet               = wire.NewSet(httpServiceContainerInjectorSet, NewServerListener, GetMigrationConfig, wire.Bind(new(controllers.ServerLifecycleListener), new(*ServerLifecycleListenerImpl)), config.ConfigInjector)
   310  	relationalDBWithControllerSet   = wire.NewSet(controllers.ControllerInjector, storage.GetNewDataAccessor, newLockRepository, newDeliveryJobRepository, newAppRepository, newChannelRepository, newProducerRepository, newConsumerRepository, newMessageRepository, dispatcher.DispatcherInjector)
   311  )