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 )