Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions cmd/icinga-notifications/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import (
)

func main() {
daemon.ParseFlagsAndConfig()
conf := daemon.Config()
conf := daemon.ParseFlagsAndConfig()

logs, err := logging.NewLoggingFromConfig("icinga-notifications", conf.Logging)
if err != nil {
Expand All @@ -45,17 +44,19 @@ func main() {
logger.Fatalf("Cannot connect to the database: %+v", err)
}

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

runtimeConfig := config.NewRuntimeConfig(logs, db)
resources := config.MakeResources(nil, conf, db, logs)
runtimeConfig := config.NewRuntimeConfig(resources)
resources.RuntimeConfig = runtimeConfig
if err := runtimeConfig.UpdateFromDatabase(ctx); err != nil {
logger.Fatalf("Failed to load config from database %+v", err)
}

go runtimeConfig.PeriodicUpdates(ctx, 1*time.Second)

err = incident.LoadOpenIncidents(ctx, db, logs.GetChildLogger("incident"), runtimeConfig)
if err != nil {
logger.Info("Loading all active incidents from database")
if err = incident.LoadOpenIncidents(ctx, resources); err != nil {
logger.Fatalf("Cannot load incidents from database: %+v", err)
}

Expand All @@ -68,7 +69,7 @@ func main() {
// When Icinga Notifications is started by systemd, we've to notify systemd that we're ready.
_ = sdnotify.Ready()

if err := listener.NewListener(db, runtimeConfig, logs).Run(ctx); err != nil {
if err := listener.NewListener(resources).Run(ctx); err != nil {
logger.Errorf("Listener has finished with an error: %+v", err)
} else {
logger.Info("Listener has finished")
Expand Down
17 changes: 9 additions & 8 deletions internal/channel/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"github.com/icinga/icinga-go-library/notifications/plugin"
"github.com/icinga/icinga-notifications/internal/config/baseconf"
"github.com/icinga/icinga-notifications/internal/contracts"
"github.com/icinga/icinga-notifications/internal/daemon"
"github.com/icinga/icinga-notifications/internal/event"
"github.com/icinga/icinga-notifications/internal/recipient"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/url"
)

type Channel struct {
Expand All @@ -23,8 +23,9 @@ type Channel struct {

Logger *zap.SugaredLogger `db:"-"`

restartCh chan newConfig
pluginCh chan *Plugin
daemonConfig *daemon.ConfigFile
restartCh chan newConfig
pluginCh chan *Plugin

pluginCtx context.Context
pluginCtxCancel func()
Expand All @@ -50,8 +51,9 @@ type newConfig struct {
}

// Start initializes the channel and starts the plugin in the background
func (c *Channel) Start(ctx context.Context, logger *zap.SugaredLogger) {
func (c *Channel) Start(ctx context.Context, conf *daemon.ConfigFile, logger *zap.SugaredLogger) {
c.Logger = logger.With(zap.Object("channel", c))
c.daemonConfig = conf
c.restartCh = make(chan newConfig)
c.pluginCh = make(chan *Plugin)
c.pluginCtx, c.pluginCtxCancel = context.WithCancel(ctx)
Expand All @@ -63,7 +65,7 @@ func (c *Channel) Start(ctx context.Context, logger *zap.SugaredLogger) {
func (c *Channel) initPlugin(cType string, config string) *Plugin {
c.Logger.Debug("Initializing channel plugin")

p, err := NewPlugin(cType, c.Logger)
p, err := NewPlugin(cType, c.daemonConfig.ChannelsDir, c.Logger)
if err != nil {
c.Logger.Errorw("Failed to initialize channel plugin", zap.Error(err))
return nil
Expand Down Expand Up @@ -160,7 +162,7 @@ func (c *Channel) Restart(logger *zap.SugaredLogger) {
}

// Notify prepares and sends the notification request, returns a non-error on fails, nil on success
func (c *Channel) Notify(contact *recipient.Contact, i contracts.Incident, ev *event.Event, icingaweb2Url string) error {
func (c *Channel) Notify(contact *recipient.Contact, i contracts.Incident, ev *event.Event) error {
p := c.getPlugin()
if p == nil {
return errors.New("plugin could not be started")
Expand All @@ -171,8 +173,7 @@ func (c *Channel) Notify(contact *recipient.Contact, i contracts.Incident, ev *e
contactStruct.Addresses = append(contactStruct.Addresses, &plugin.Address{Type: addr.Type, Address: addr.Address})
}

baseUrl, _ := url.Parse(icingaweb2Url)
incidentUrl := baseUrl.JoinPath("/notifications/incident")
incidentUrl := c.daemonConfig.IcingaWeb2UrlParsed.JoinPath("/notifications/incident")
incidentUrl.RawQuery = fmt.Sprintf("id=%d", i.ID())
object := i.IncidentObject()

Expand Down
10 changes: 5 additions & 5 deletions internal/channel/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ type Plugin struct {
}

// NewPlugin starts and returns a new plugin instance. If the start of the plugin fails, an error is returned
func NewPlugin(pluginType string, logger *zap.SugaredLogger) (*Plugin, error) {
file := filepath.Join(daemon.Config().ChannelsDir, pluginType)
func NewPlugin(pluginType, channelsDir string, logger *zap.SugaredLogger) (*Plugin, error) {
file := filepath.Join(channelsDir, pluginType)

logger.Debugw("Starting new channel plugin process", zap.String("path", file))

Expand Down Expand Up @@ -167,9 +167,9 @@ func forwardLogs(errPipe io.Reader, logger *zap.SugaredLogger) {
}

// UpsertPlugins upsert the available_channel_type table with working plugins
func UpsertPlugins(ctx context.Context, channelPluginDir string, logger *logging.Logger, db *database.DB) {
func UpsertPlugins(ctx context.Context, conf *daemon.ConfigFile, logger *logging.Logger, db *database.DB) {
logger.Debug("Updating available channel types")
files, err := os.ReadDir(channelPluginDir)
files, err := os.ReadDir(conf.ChannelsDir)
if err != nil {
logger.Errorw("Failed to read the channel plugin directory", zap.Error(err))
}
Expand All @@ -185,7 +185,7 @@ func UpsertPlugins(ctx context.Context, channelPluginDir string, logger *logging
continue
}

p, err := NewPlugin(pluginType, pluginLogger)
p, err := NewPlugin(pluginType, conf.ChannelsDir, pluginLogger)
if err != nil {
pluginLogger.Errorw("Failed to start plugin", zap.Error(err))
continue
Expand Down
4 changes: 2 additions & 2 deletions internal/config/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ func (r *RuntimeConfig) applyPendingChannels() {
r,
&r.Channels, &r.configChange.Channels,
func(newElement *channel.Channel) error {
newElement.Start(context.TODO(), r.logs.GetChildLogger("channel").SugaredLogger)
newElement.Start(context.TODO(), r.DaemonConfig, r.Logs.GetChildLogger("channel").SugaredLogger)
return nil
},
func(curElement, update *channel.Channel) error {
curElement.ChangedAt = update.ChangedAt
curElement.Name = update.Name
curElement.Type = update.Type
curElement.Config = update.Config
curElement.Restart(r.logs.GetChildLogger("channel").SugaredLogger)
curElement.Restart(r.Logs.GetChildLogger("channel").SugaredLogger)
return nil
},
func(delElement *channel.Channel) error {
Expand Down
4 changes: 2 additions & 2 deletions internal/config/incremental_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func incrementalFetch[
stmtLogger := r.logger.With(zap.String("table", tableName))

var (
stmt = r.db.BuildSelectStmt(typePtr, typePtr)
stmt = r.DB.BuildSelectStmt(typePtr, typePtr)
stmtArgs []any
)
if hasChangedAt {
Expand All @@ -67,7 +67,7 @@ func incrementalFetch[
stmtArgs = []any{changedAt}
}

stmt = r.db.Rebind(stmt + ` ORDER BY "changed_at"`)
stmt = r.DB.Rebind(stmt + ` ORDER BY "changed_at"`)
stmtLogger = stmtLogger.With(zap.String("query", stmt))

var ts []T
Expand Down
33 changes: 23 additions & 10 deletions internal/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/icinga/icinga-go-library/logging"
"github.com/icinga/icinga-go-library/types"
"github.com/icinga/icinga-notifications/internal/channel"
"github.com/icinga/icinga-notifications/internal/daemon"
"github.com/icinga/icinga-notifications/internal/recipient"
"github.com/icinga/icinga-notifications/internal/rule"
"github.com/icinga/icinga-notifications/internal/timeperiod"
Expand All @@ -19,12 +20,30 @@ import (
"time"
)

// Resources holds references to commonly used objects.
type Resources struct {
DaemonConfig *daemon.ConfigFile `db:"-" json:"-"`
RuntimeConfig *RuntimeConfig `db:"-" json:"-"`
DB *database.DB `db:"-" json:"-"`
Logs *logging.Logging `db:"-" json:"-"`
}

// MakeResources creates a new Resources instance with the provided components.
//
// This function initializes the Resources struct by assigning the given [RuntimeConfig],
// database connection, and logging facilities.
func MakeResources(rc *RuntimeConfig, file *daemon.ConfigFile, db *database.DB, logs *logging.Logging) *Resources {
return &Resources{RuntimeConfig: rc, DaemonConfig: file, DB: db, Logs: logs}
}

// RuntimeConfig stores the runtime representation of the configuration present in the database.
type RuntimeConfig struct {
// ConfigSet is the current live config. It is embedded to allow direct access to its members.
// Accessing it requires a lock that is obtained with RLock() and released with RUnlock().
ConfigSet

*Resources // Contains references to commonly used objects and to itself.

// configChange contains incremental changes to config objects to be merged into the live configuration.
//
// It will be both created and deleted within RuntimeConfig.UpdateFromDatabase. To keep track of the known state,
Expand All @@ -33,24 +52,18 @@ type RuntimeConfig struct {
configChangeAvailable bool
configChangeTimestamps map[string]types.UnixMilli

logs *logging.Logging
logger *logging.Logger
db *database.DB

// mu is used to synchronize access to the live ConfigSet.
mu sync.RWMutex
}

func NewRuntimeConfig(
logs *logging.Logging,
db *database.DB,
) *RuntimeConfig {
func NewRuntimeConfig(resouces *Resources) *RuntimeConfig {
return &RuntimeConfig{
configChangeTimestamps: make(map[string]types.UnixMilli),

logs: logs,
logger: logs.GetChildLogger("runtime-updates"),
db: db,
Resources: resouces,
logger: resouces.Logs.GetChildLogger("runtime-updates"),
}
}

Expand Down Expand Up @@ -235,7 +248,7 @@ func (r *RuntimeConfig) GetSourceFromCredentials(user, pass string, logger *logg
}

func (r *RuntimeConfig) fetchFromDatabase(ctx context.Context) error {
tx, err := r.db.BeginTxx(ctx, &sql.TxOptions{
tx, err := r.DB.BeginTxx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
ReadOnly: true,
})
Expand Down
39 changes: 22 additions & 17 deletions internal/daemon/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package daemon

import (
"errors"
"fmt"
"github.com/creasty/defaults"
"github.com/icinga/icinga-go-library/config"
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/logging"
"github.com/icinga/icinga-go-library/utils"
"github.com/icinga/icinga-notifications/internal"
"net/url"
"os"
"time"
)
Expand All @@ -25,6 +27,12 @@ type ConfigFile struct {
Icingaweb2URL string `yaml:"icingaweb2-url"`
Database database.Config `yaml:"database"`
Logging logging.Config `yaml:"logging"`

// IcingaWeb2UrlParsed holds the parsed Icinga Web 2 URL after validation of the config file.
//
// This field is not part of the YAML config and is only populated after successful validation.
// The resulting URL always ends with a trailing slash, making it easier to resolve relative paths against it.
IcingaWeb2UrlParsed *url.URL
}

// SetDefaults implements the defaults.Setter interface.
Expand All @@ -44,6 +52,15 @@ func (c *ConfigFile) Validate() error {
return err
}

if c.Icingaweb2URL == "" {
return errors.New("icingaweb2-url must be set")
}

parsedUrl, err := url.Parse(c.Icingaweb2URL)
if err != nil {
return fmt.Errorf("invalid icingaweb2-url: %w", err)
}
c.IcingaWeb2UrlParsed = parsedUrl.JoinPath("/")
return nil
}

Expand All @@ -61,23 +78,10 @@ type Flags struct {
Config string `short:"c" long:"config" description:"path to config file"`
}

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

// Config returns the config that was loaded while starting the daemon.
// Panics when ParseFlagsAndConfig was not called earlier.
func Config() *ConfigFile {
if daemonConfig == nil {
panic("ERROR: daemon.Config() called before daemon.ParseFlagsAndConfig()")
}

return daemonConfig
}

// ParseFlagsAndConfig parses the CLI flags provided to the executable and tries to load the config from the YAML file.
// Prints any error during parsing or config loading to os.Stderr and exits.
func ParseFlagsAndConfig() {
//
// Prints any error during parsing or config loading to os.Stderr and exits, otherwise returns the loaded ConfigFile.
func ParseFlagsAndConfig() *ConfigFile {
flags := Flags{Config: internal.SysConfDir + "/icinga-notifications/config.yml"}
if err := config.ParseFlags(&flags); err != nil {
if errors.Is(err, config.ErrInvalidArgument) {
Expand All @@ -92,12 +96,13 @@ func ParseFlagsAndConfig() {
os.Exit(ExitSuccess)
}

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

utils.PrintErrorThenExit(err, ExitFailure)
}
return daemonConfig
}
12 changes: 6 additions & 6 deletions internal/event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ type Event struct {
}

// CompleteURL prefixes the URL with the given Icinga Web 2 base URL unless it already carries a URL or is empty.
func (e *Event) CompleteURL(icingaWebBaseUrl string) {
func (e *Event) CompleteURL(icingaWebBaseUrl *url.URL) {
if e.URL == "" {
return
}

if !strings.HasSuffix(icingaWebBaseUrl, "/") {
icingaWebBaseUrl += "/"
u, err := url.Parse(strings.TrimLeft(e.URL, "/"))
if err != nil {
return // leave as is on parse error
}

u, err := url.Parse(e.URL)
if err != nil || u.Scheme == "" {
e.URL = icingaWebBaseUrl + e.URL
if !u.IsAbs() {
e.URL = icingaWebBaseUrl.ResolveReference(u).String()
}
}

Expand Down
Loading
Loading