Skip to content

Commit

Permalink
Cleanup main func & use ctx everywhere
Browse files Browse the repository at this point in the history
The CLI parsing utilities are already provided by our GO library, so
this has been removed from the `main` function. The `context.Context`
related changes are necessary as otherwise you will have to wait for
`5m` when having Icinga Notifications started with invalid database
Config due to the internal database.Connect retrials. Lastly, the
logging related changes are useful as we want to properly render the
stack traces when we unexpectedly exit from the main function instead of
wrapping the error in a confusing `zap.Error()` json key.
  • Loading branch information
yhabteab committed May 27, 2024
1 parent a90a23e commit 83e6b58
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 104 deletions.
65 changes: 17 additions & 48 deletions cmd/icinga-notifications-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package main

import (
"context"
"flag"
"fmt"
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/logging"
"github.com/icinga/icinga-go-library/utils"
Expand All @@ -14,72 +12,43 @@ import (
"github.com/icinga/icinga-notifications/internal/icinga2"
"github.com/icinga/icinga-notifications/internal/incident"
"github.com/icinga/icinga-notifications/internal/listener"
"go.uber.org/zap"
"os"
"github.com/pkg/errors"
"os/signal"
"runtime"
"syscall"
"time"
)

func main() {
var configPath string
var showVersion bool

flag.StringVar(&configPath, "config", "", "path to config file")
flag.BoolVar(&showVersion, "version", false, "print version")
flag.Parse()

if showVersion {
// reuse internal.Version.print() once the project name is configurable
fmt.Println("Icinga Notifications version:", internal.Version.Version)
fmt.Println()

fmt.Println("Build information:")
fmt.Printf(" Go version: %s (%s, %s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
if internal.Version.Commit != "" {
fmt.Println(" Git commit:", internal.Version.Commit)
}
return
}

if configPath == "" {
_, _ = fmt.Fprintln(os.Stderr, "missing -config flag")
os.Exit(1)
}

err := daemon.LoadConfig(configPath)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "cannot load config:", err)
os.Exit(1)
}
// Parse the CLI flags and load/initialise the configuration file.
daemon.Init()

conf := daemon.Config()

logs, err := logging.NewLoggingFromConfig("icinga-notifications", conf.Logging)
if err != nil {
utils.PrintErrorThenExit(err, 1)
utils.PrintErrorThenExit(err, daemon.ExitFailure)
}

logger := logs.GetLogger()
defer func() { _ = logger.Sync() }()

logger.Infof("Starting Icinga Notifications daemon (%s)", internal.Version.Version)
db, err := database.NewDbFromConfig(&conf.Database, logs.GetChildLogger("database"), database.RetryConnectorCallbacks{})
if err != nil {
logger.Fatalw("cannot create database connection from config", zap.Error(err))
logger.Fatalf("%+v", errors.Wrap(err, "cannot create database connection from config"))
}
defer db.Close()
{
logger.Infof("Connecting to database at '%s'", db.GetAddr())
if err := db.Ping(); err != nil {
logger.Fatalw("cannot connect to database", zap.Error(err))
}
}

channel.UpsertPlugins(conf.ChannelPluginDir, logs.GetChildLogger("channel"), db)

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

logger.Infof("Connecting to database at '%s'", db.GetAddr())
if err := db.PingContext(ctx); err != nil {
logger.Fatalf("%+v", err)
}

channel.UpsertPlugins(ctx, conf.ChannelPluginDir, logs.GetChildLogger("channel"), db)

icinga2Launcher := &icinga2.Launcher{
Ctx: ctx,
Logs: logs,
Expand All @@ -89,7 +58,7 @@ func main() {

runtimeConfig := config.NewRuntimeConfig(icinga2Launcher.Launch, logs, db)
if err := runtimeConfig.UpdateFromDatabase(ctx); err != nil {
logger.Fatalw("failed to load config from database", zap.Error(err))
logger.Fatalf("%+v", errors.Wrap(err, "failed to load config from database"))
}

icinga2Launcher.RuntimeConfig = runtimeConfig
Expand All @@ -98,13 +67,13 @@ func main() {

err = incident.LoadOpenIncidents(ctx, db, logs.GetChildLogger("incident"), runtimeConfig)
if err != nil {
logger.Fatalw("Can't load incidents from database", zap.Error(err))
logger.Fatalf("%+v", errors.Wrap(err, "cannot load incidents from database"))
}

// Wait to load open incidents from the database before either starting Event Stream Clients or starting the Listener.
icinga2Launcher.Ready()
if err := listener.NewListener(db, runtimeConfig, logs).Run(ctx); err != nil {
logger.Errorw("Listener has finished with an error", zap.Error(err))
logger.Errorf("%+v", errors.Wrap(err, "listener has finished with an error"))
} else {
logger.Info("Listener has finished")
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ require (
github.com/creasty/defaults v1.7.0
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
github.com/emersion/go-smtp v0.21.2
github.com/goccy/go-yaml v1.11.3
github.com/google/uuid v1.6.0
github.com/icinga/icinga-go-library v0.1.1-0.20240527065833-cfb45a9e3db2
github.com/jhillyerd/enmime v1.2.0
Expand All @@ -27,6 +26,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-yaml v1.11.3 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect
Expand Down
5 changes: 3 additions & 2 deletions internal/channel/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package channel

import (
"bufio"
"context"
"encoding/json"
"fmt"
"github.com/icinga/icinga-go-library/database"
Expand Down Expand Up @@ -166,7 +167,7 @@ func forwardLogs(errPipe io.Reader, logger *zap.SugaredLogger) {
}

// UpsertPlugins upsert the available_channel_type table with working plugins
func UpsertPlugins(channelPluginDir string, logger *logging.Logger, db *database.DB) {
func UpsertPlugins(ctx context.Context, channelPluginDir string, logger *logging.Logger, db *database.DB) {
logger.Debug("Updating available channel types")
files, err := os.ReadDir(channelPluginDir)
if err != nil {
Expand Down Expand Up @@ -209,7 +210,7 @@ func UpsertPlugins(channelPluginDir string, logger *logging.Logger, db *database
}

stmt, _ := db.BuildUpsertStmt(&plugin.Info{})
_, err = db.NamedExec(stmt, pluginInfos)
_, err = db.NamedExecContext(ctx, stmt, pluginInfos)
if err != nil {
logger.Errorw("Failed to update available channel types", zap.Error(err))
} else {
Expand Down
61 changes: 8 additions & 53 deletions internal/daemon/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package daemon

import (
"errors"
"github.com/creasty/defaults"
"github.com/goccy/go-yaml"
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/logging"
"os"
)

type ConfigFile struct {
Expand All @@ -18,55 +14,6 @@ type ConfigFile struct {
Logging logging.Config `yaml:"logging"`
}

// config holds the configuration state as a singleton. It is used from LoadConfig and Config
var config *ConfigFile

// LoadConfig loads the daemon config from given path. Call it only once when starting the daemon.
func LoadConfig(path string) error {
if config != nil {
return errors.New("config already set")
}

cfg, err := fromFile(path)
if err != nil {
return err
}

config = cfg

return nil
}

// Config returns the config that was loaded while starting the daemon
func Config() *ConfigFile {
return config
}

func fromFile(path string) (*ConfigFile, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()

var c ConfigFile

if err := defaults.Set(&c); err != nil {
return nil, err
}

d := yaml.NewDecoder(f)
if err := d.Decode(&c); err != nil {
return nil, err
}

if err := c.Validate(); err != nil {
return nil, err
}

return &c, nil
}

func (c *ConfigFile) Validate() error {
if err := c.Database.Validate(); err != nil {
return err
Expand All @@ -77,3 +24,11 @@ func (c *ConfigFile) Validate() error {

return nil
}

// Flags defines the CLI flags supported by Icinga Notifications.
type Flags struct {
// Version decides whether to just print the version and exit.
Version bool `long:"version" description:"print version and exit"`
// Config is the path to the config file
Config string `short:"c" long:"config" description:"path to config file" required:"true" default:"/etc/icinga-notifications/config.yml"`
}
57 changes: 57 additions & 0 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package daemon

import (
"errors"
"github.com/icinga/icinga-go-library/config"
"github.com/icinga/icinga-go-library/utils"
"github.com/icinga/icinga-notifications/internal"
"os"
)

const (
ExitSuccess = 0
ExitFailure = 1
)

// daemonConfig holds the configuration state as a singleton.
// It is initialised by the [Init] func and exposed through the [Config] function.
var daemonConfig *ConfigFile

// Config returns the config that was loaded while starting the daemon.
// Panics when the global *[ConfigFile] instance isn't initialised yet.
func Config() *ConfigFile {
if daemonConfig == nil {
panic("ERROR: daemon.Config not initialized")
}

return daemonConfig
}

// Init first parses the CLI flags provided to the executable and tries to load the config from the YAML file.
// Init prints any error during parsing or config loading to [os.Stderr] and exits.
func Init() {
var flags Flags
if err := config.ParseFlags(&flags); err != nil {
if errors.Is(err, config.ErrInvalidArgument) {
panic(err)
}

utils.PrintErrorThenExit(err, ExitFailure)
}

if flags.Version {
internal.Version.Print()
os.Exit(ExitSuccess)
}

cfg := new(ConfigFile)
if err := config.FromYAMLFile(flags.Config, cfg); err != nil {
if errors.Is(err, config.ErrInvalidArgument) {
panic(err)
}

utils.PrintErrorThenExit(err, ExitFailure)
}

daemonConfig = cfg
}

0 comments on commit 83e6b58

Please sign in to comment.