diff --git a/internal/services/icinga2/docker.go b/internal/services/icinga2/docker.go index c472a71..5f1d669 100644 --- a/internal/services/icinga2/docker.go +++ b/internal/services/icinga2/docker.go @@ -189,6 +189,10 @@ func (n *dockerInstance) EnableIcingaDb(redis services.RedisServerBase) { services.Icinga2{Icinga2Base: n}.WriteIcingaDbConf(redis) } +func (n *dockerInstance) EnableIcingaNotifications(notis services.IcingaNotificationsBase) { + services.Icinga2{Icinga2Base: n}.WriteIcingaNotificationsConf(notis) +} + func (n *dockerInstance) Cleanup() { n.icinga2Docker.runningMutex.Lock() delete(n.icinga2Docker.running, n) diff --git a/internal/services/icingadb/docker_binary.go b/internal/services/icingadb/docker_binary.go index 4f28c92..dd3b810 100644 --- a/internal/services/icingadb/docker_binary.go +++ b/internal/services/icingadb/docker_binary.go @@ -11,7 +11,6 @@ import ( "github.com/icinga/icinga-testing/services" "github.com/icinga/icinga-testing/utils" "go.uber.org/zap" - "io/ioutil" "os" "path/filepath" "sync" @@ -67,7 +66,7 @@ func (i *dockerBinaryCreator) CreateIcingaDb( icingaDbDockerBinary: i, } - configFile, err := ioutil.TempFile("", "icingadb.yml") + configFile, err := os.CreateTemp("", "icingadb.yml") if err != nil { panic(err) } diff --git a/internal/services/notifications/docker.go b/internal/services/notifications/docker.go new file mode 100644 index 0000000..596e78a --- /dev/null +++ b/internal/services/notifications/docker.go @@ -0,0 +1,200 @@ +package notifications + +import ( + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/icinga/icinga-testing/services" + "github.com/icinga/icinga-testing/utils" + "go.uber.org/zap" + "os" + "path/filepath" + "sync" + "sync/atomic" +) + +type dockerBinaryCreator struct { + logger *zap.Logger + dockerClient *client.Client + dockerNetworkId string + containerNamePrefix string + binaryPath string + channelDirPath string + containerCounter uint32 + + runningMutex sync.Mutex + running map[*dockerBinaryInstance]struct{} +} + +var _ Creator = (*dockerBinaryCreator)(nil) + +func NewDockerBinaryCreator( + logger *zap.Logger, + dockerClient *client.Client, + containerNamePrefix string, + dockerNetworkId string, + binaryPath string, + channelDirPath string, +) Creator { + binaryPath, err := filepath.Abs(binaryPath) + if err != nil { + panic(err) + } + return &dockerBinaryCreator{ + logger: logger.With(zap.Bool("icinga_notifications", true)), + dockerClient: dockerClient, + dockerNetworkId: dockerNetworkId, + containerNamePrefix: containerNamePrefix, + binaryPath: binaryPath, + channelDirPath: channelDirPath, + running: make(map[*dockerBinaryInstance]struct{}), + } +} + +func (i *dockerBinaryCreator) CreateIcingaNotifications( + rdb services.RelationalDatabase, + options ...services.IcingaNotificationsOption, +) services.IcingaNotificationsBase { + inst := &dockerBinaryInstance{ + info: info{rdb: rdb}, + logger: i.logger, + icingaNotificationsDockerBinary: i, + } + + configFile, err := os.CreateTemp("", "icinga_notifications.yml") + if err != nil { + panic(err) + } + idb := &services.IcingaNotifications{IcingaNotificationsBase: inst} + for _, option := range options { + option(idb) + } + if err = idb.WriteConfig(configFile); err != nil { + panic(err) + } + inst.configFileName = configFile.Name() + err = configFile.Close() + if err != nil { + panic(err) + } + + containerName := fmt.Sprintf("%s-%d", i.containerNamePrefix, atomic.AddUint32(&i.containerCounter, 1)) + inst.logger = inst.logger.With(zap.String("container-name", containerName)) + networkName, err := utils.DockerNetworkName(context.Background(), i.dockerClient, i.dockerNetworkId) + if err != nil { + panic(err) + } + + dockerImage := "alpine:latest" + err = utils.DockerImagePull(context.Background(), inst.logger, i.dockerClient, dockerImage, false) + if err != nil { + panic(err) + } + + cont, err := i.dockerClient.ContainerCreate(context.Background(), &container.Config{ + Cmd: []string{"/icinga-notifications-daemon", "-config", "/icinga_notifications.yml"}, + Image: dockerImage, + ExposedPorts: map[nat.Port]struct{}{nat.Port(defaultPort + "/tcp"): {}}, + }, &container.HostConfig{ + Mounts: []mount.Mount{{ + Type: mount.TypeBind, + Source: i.binaryPath, + Target: "/icinga-notifications-daemon", + ReadOnly: true, + }, { + Type: mount.TypeBind, + Source: i.channelDirPath, + Target: "/channel", + ReadOnly: true, + }, { + Type: mount.TypeBind, + Source: inst.configFileName, + Target: "/icinga_notifications.yml", + ReadOnly: true, + }}, + }, &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + networkName: { + NetworkID: i.dockerNetworkId, + }, + }, + }, nil, containerName) + if err != nil { + inst.logger.Fatal("failed to create icinga-notifications container", zap.Error(err)) + } + inst.containerId = cont.ID + inst.logger = inst.logger.With(zap.String("container-id", cont.ID)) + inst.logger.Debug("created container") + + err = utils.ForwardDockerContainerOutput(context.Background(), i.dockerClient, cont.ID, + false, utils.NewLineWriter(func(line []byte) { + inst.logger.Debug("container output", + zap.ByteString("line", line)) + })) + if err != nil { + inst.logger.Fatal("failed to attach to container output", zap.Error(err)) + } + + err = i.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) + if err != nil { + inst.logger.Fatal("failed to start container", zap.Error(err)) + } + inst.logger.Debug("started container") + + inst.info.host = utils.MustString(utils.DockerContainerAddress(context.Background(), i.dockerClient, cont.ID)) + inst.info.port = defaultPort + + i.runningMutex.Lock() + i.running[inst] = struct{}{} + i.runningMutex.Unlock() + + return inst +} + +func (i *dockerBinaryCreator) Cleanup() { + i.runningMutex.Lock() + instances := make([]*dockerBinaryInstance, 0, len(i.running)) + for inst := range i.running { + instances = append(instances, inst) + } + i.runningMutex.Unlock() + + for _, inst := range instances { + inst.Cleanup() + } +} + +type dockerBinaryInstance struct { + info + icingaNotificationsDockerBinary *dockerBinaryCreator + logger *zap.Logger + containerId string + configFileName string +} + +var _ services.IcingaNotificationsBase = (*dockerBinaryInstance)(nil) + +func (i *dockerBinaryInstance) Cleanup() { + i.icingaNotificationsDockerBinary.runningMutex.Lock() + delete(i.icingaNotificationsDockerBinary.running, i) + i.icingaNotificationsDockerBinary.runningMutex.Unlock() + + err := i.icingaNotificationsDockerBinary.dockerClient.ContainerRemove(context.Background(), i.containerId, types.ContainerRemoveOptions{ + Force: true, + RemoveVolumes: true, + }) + if err != nil { + panic(err) + } + i.logger.Debug("removed container") + + err = os.Remove(i.configFileName) + if err != nil { + panic(err) + } +} diff --git a/internal/services/notifications/notifications.go b/internal/services/notifications/notifications.go new file mode 100644 index 0000000..87eb582 --- /dev/null +++ b/internal/services/notifications/notifications.go @@ -0,0 +1,33 @@ +package notifications + +import ( + "github.com/icinga/icinga-testing/services" +) + +// defaultPort of the Icinga Notifications Web Listener. +const defaultPort string = "5680" + +type Creator interface { + CreateIcingaNotifications(rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption) services.IcingaNotificationsBase + Cleanup() +} + +// info provides a partial implementation of the services.IcingaNotificationsBase interface. +type info struct { + host string + port string + + rdb services.RelationalDatabase +} + +func (i *info) Host() string { + return i.host +} + +func (i *info) Port() string { + return i.port +} + +func (i *info) RelationalDatabase() services.RelationalDatabase { + return i.rdb +} diff --git a/it.go b/it.go index 79e96e5..4204cc8 100644 --- a/it.go +++ b/it.go @@ -13,6 +13,11 @@ // must be compiled using CGO_ENABLED=0 // - ICINGA_TESTING_ICINGADB_SCHEMA_MYSQL: Path to the full Icinga DB schema file for MySQL/MariaDB // - ICINGA_TESTING_ICINGADB_SCHEMA_PGSQL: Path to the full Icinga DB schema file for PostgreSQL +// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY: Path to the Icinga Notifications binary to test. It will run in a +// container and therefore must be compiled using CGO_ENABLED=0 +// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR: Path to the Icinga Notifications channel binary directory. Those +// are also needed to be compiled with CGO_ENABLED=0. +// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL: Path to the full Icinga Notifications PostgreSQL schema file package icingatesting import ( @@ -24,6 +29,7 @@ import ( "github.com/icinga/icinga-testing/internal/services/icinga2" "github.com/icinga/icinga-testing/internal/services/icingadb" "github.com/icinga/icinga-testing/internal/services/mysql" + "github.com/icinga/icinga-testing/internal/services/notifications" "github.com/icinga/icinga-testing/internal/services/postgresql" "github.com/icinga/icinga-testing/internal/services/redis" "github.com/icinga/icinga-testing/services" @@ -50,18 +56,19 @@ import ( // m.Run() // } type IT struct { - mutex sync.Mutex - deferredCleanup []func() - prefix string - dockerClient *client.Client - dockerNetworkId string - mysql mysql.Creator - postgresql postgresql.Creator - redis redis.Creator - icinga2 icinga2.Creator - icingaDb icingadb.Creator - logger *zap.Logger - loggerDebugCore zapcore.Core + mutex sync.Mutex + deferredCleanup []func() + prefix string + dockerClient *client.Client + dockerNetworkId string + mysql mysql.Creator + postgresql postgresql.Creator + redis redis.Creator + icinga2 icinga2.Creator + icingaDb icingadb.Creator + icingaNotifications notifications.Creator + logger *zap.Logger + loggerDebugCore zapcore.Core } var flagDebugLog = flag.String("icingatesting.debuglog", "", "file to write debug log to") @@ -288,6 +295,55 @@ func (it *IT) IcingaDbInstanceT( return i } +func (it *IT) getIcingaNotifications() notifications.Creator { + keys := map[string]string{ + "ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY": "", + "ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR": "", + } + + for key := range keys { + var ok bool + keys[key], ok = os.LookupEnv(key) + if !ok { + panic(fmt.Errorf("environment variable %s must be set", key)) + } + } + + it.mutex.Lock() + defer it.mutex.Unlock() + + if it.icingaNotifications == nil { + it.icingaNotifications = notifications.NewDockerBinaryCreator( + it.logger, it.dockerClient, it.prefix+"-icinga-notifications", it.dockerNetworkId, + keys["ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY"], keys["ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR"]) + it.deferCleanup(it.icingaNotifications.Cleanup) + } + + return it.icingaNotifications +} + +// IcingaNotificationsInstance starts a new Icinga Notifications instance. +// +// It expects the ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY environment variable to be set to the path of a precompiled +// binary which is then started in a new Docker container when this function is called. The environment variable +// ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR needs also to be set to a directory holding the channel binaries. +func (it *IT) IcingaNotificationsInstance( + rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption, +) services.IcingaNotifications { + return services.IcingaNotifications{ + IcingaNotificationsBase: it.getIcingaNotifications().CreateIcingaNotifications(rdb, options...), + } +} + +// IcingaNotificationsInstanceT creates a new Icinga Notifications instance and registers its cleanup function with testing.T. +func (it *IT) IcingaNotificationsInstanceT( + t testing.TB, rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption, +) services.IcingaNotifications { + i := it.IcingaNotificationsInstance(rdb, options...) + t.Cleanup(i.Cleanup) + return i +} + // Logger returns a *zap.Logger which additionally logs the current test case name. func (it *IT) Logger(t testing.TB) *zap.Logger { cores := []zapcore.Core{zaptest.NewLogger(t, zaptest.WrapOptions(zap.IncreaseLevel(zap.InfoLevel))).Core()} diff --git a/services/icinga2.go b/services/icinga2.go index f1c461b..a548b4c 100644 --- a/services/icinga2.go +++ b/services/icinga2.go @@ -40,6 +40,9 @@ type Icinga2Base interface { // EnableIcingaDb enables the icingadb feature on this node using the connection details of redis. EnableIcingaDb(redis RedisServerBase) + // EnableIcingaNotifications enables the Icinga Notifications integration with the custom configuration. + EnableIcingaNotifications(IcingaNotificationsBase) + // Cleanup stops the node and removes everything that was created to start this node. Cleanup() } @@ -128,3 +131,16 @@ func (i Icinga2) WriteIcingaDbConf(r RedisServerBase) { } i.WriteConfig(fmt.Sprintf("etc/icinga2/features-enabled/icingadb_%s_%s.conf", r.Host(), r.Port()), b.Bytes()) } + +//go:embed icinga2_icinga_notifications.conf +var icinga2IcingaNotificationsConfRawTemplate string +var icinga2IcingaNotificationsConfTemplate = template.Must(template.New("icinga-notifications.conf").Parse(icinga2IcingaNotificationsConfRawTemplate)) + +func (i Icinga2) WriteIcingaNotificationsConf(notis IcingaNotificationsBase) { + b := bytes.NewBuffer(nil) + err := icinga2IcingaNotificationsConfTemplate.Execute(b, notis) + if err != nil { + panic(err) + } + i.WriteConfig("etc/icinga2/features-enabled/icinga_notifications.conf", b.Bytes()) +} diff --git a/services/icinga2_icinga_notifications.conf b/services/icinga2_icinga_notifications.conf new file mode 100644 index 0000000..5dbbbe5 --- /dev/null +++ b/services/icinga2_icinga_notifications.conf @@ -0,0 +1,290 @@ +const IcingaNotificationsProcessEventUrl = "http://{{.Host}}:{{.Port}}/process-event" +const IcingaNotificationsIcingaWebUrl = "http://localhost/icingaweb2" +const IcingaNotificationsAuth = "source-1:correct horse battery staple" + +// urlencode a string loosely based on RFC 3986. +// +// Char replacement will be performed through a simple lookup table based on +// the RFC's chapters 2.2 and 2.3. This, however, is limited to ASCII. +function urlencode(str) { + var replacement = { + // gen-delims + ":" = "%3A", "/" = "%2F", "?" = "%3F", "#" = "%23", "[" = "%5B", "]" = "%5D", "@" = "%40" + + // sub-delims + "!" = "%21", "$" = "%24", "&" = "%26", "'" = "%27", "(" = "%28", ")" = "%29" + "*" = "%2A", "+" = "%2B", "," = "%2C", ";" = "%3B", "=" = "%3D" + + // additionals based on !unreserved + "\n" = "%0A", "\r" = "%0D", " " = "%20", "\"" = "%22" + } + + var pos = 0 + var out = "" + + while (pos < str.len()) { + var cur = str.substr(pos, 1) + out += replacement.contains(cur) ? replacement.get(cur) : cur + pos += 1 + } + + return out +} + +object User "icinga-notifications" { + # Workaround, types filter here must exclude Problem, otherwise no Acknowledgement notifications are sent. + # https://github.com/Icinga/icinga2/issues/9739 + types = [ Acknowledgement ] +} + +var baseBody = { + "curl" = { + order = -1 + set_if = {{`{{ true }}`}} + skip_key = true + value = {{`{{ + // Only send events that have either severity or type set, otherwise make it a no-op by executing true. + // This is used for preventing the EventCommand from sending invalid events for soft states. + (len(macro("$event_severity$")) > 0 || len(macro("$event_type$")) > 0) ? "curl" : "true" + }}`}} + } + "--user" = { value = IcingaNotificationsAuth } + "--fail" = { set_if = true } + "--silent" = { set_if = true } + "--show-error" = { set_if = true } + "url" = { + skip_key = true + value = IcingaNotificationsProcessEventUrl + } +} + +var hostBody = baseBody + { + "-d" = {{`{{ + var args = {} + args.tags.host = macro("$event_hostname$") + args.name = macro("$event_object_name$") + args.username = macro("$event_author$") + args.message = macro("$event_message$") + args.url = IcingaNotificationsIcingaWebUrl + "/icingadb/host?name=" + urlencode(macro("$host.name$")) + + var type = macro("$event_type$") + if (len(type) > 0) { + args.type = type + } + + var severity = macro("$event_severity$") + if (len(severity) > 0) { + args.severity = severity + } + + var extraTags = macro("$event_extra_tags$") + if (extraTags.len() > 0) { + args.extra_tags = extraTags + } + + return Json.encode(args) + }}`}} +} + +var hostExtraTags = {{`{{ + var tags = {} + for (group in host.groups) { + tags.set("hostgroup/" + group, null) + } + + return tags +}}`}} + +object NotificationCommand "icinga-notifications-host" use(hostBody, hostExtraTags) { + command = [ /* full command line generated from arguments */ ] + + arguments = hostBody + + vars += { + event_hostname = "$host.name$" + event_author = "$notification.author$" + event_message = "$notification.comment$" + event_object_name = "$host.display_name$" + event_extra_tags = hostExtraTags + } + + vars.event_type = {{`{{ + if (macro("$notification.type$") == "ACKNOWLEDGEMENT") { + return "acknowledgement" + } + + return "" + }}`}} + + vars.event_severity = {{`{{ + if (macro("$notification.type$") != "ACKNOWLEDGEMENT") { + return macro("$host.state$") == "DOWN" ? "crit" : "ok" + } + + return "" + }}`}} +} + +object EventCommand "icinga-notifications-host-events" use(hostBody, hostExtraTags) { + command = [ /* full command line generated from arguments */ ] + + arguments = hostBody + + vars += { + event_hostname = "$host.name$" + event_author = "" + event_message = "$host.output$" + event_object_name = "$host.display_name$" + event_extra_tags = hostExtraTags + } + + vars.event_severity = {{`{{ + if (macro("$host.state_type$") == "HARD") { + return macro("$host.state$") == "DOWN" ? "crit" : "ok" + } + + return "" + }}`}} +} + +template Host "generic-icinga-notifications-host" default { + event_command = "icinga-notifications-host-events" +} + +apply Notification "icinga-notifications-forwarder" to Host { + command = "icinga-notifications-host" + + types = [ Acknowledgement ] + + users = [ "icinga-notifications" ] + + assign where true +} + +var serviceBody = baseBody + { + "-d" = {{`{{ + var args = {} + args.tags.host = macro("$event_hostname$") + args.tags.service = macro("$event_servicename$") + args.name = macro("$event_object_name$") + args.username = macro("$event_author$") + args.message = macro("$event_message$") + args.url = IcingaNotificationsIcingaWebUrl + "/icingadb/service?name=" + urlencode(macro("$service.name$")) + "&host.name=" + urlencode(macro("$service.host.name$")) + + var type = macro("$event_type$") + if (len(type) > 0) { + args.type = type + } + + var severity = macro("$event_severity$") + if (len(severity) > 0) { + args.severity = severity + } + + var extraTags = macro("$event_extra_tags$") + if (extraTags.len() > 0) { + args.extra_tags = extraTags + } + + return Json.encode(args) + }}`}} +} + +var serviceExtraTags = {{`{{ + var tags = {} + for (group in service.host.groups) { + tags.set("hostgroup/" + group, null) + } + + for (group in service.groups) { + tags.set("servicegroup/" + group, null) + } + + return tags +}}`}} + +object NotificationCommand "icinga-notifications-service" use(serviceBody, serviceExtraTags) { + command = [ /* full command line generated from arguments */ ] + + arguments = serviceBody + + vars += { + event_hostname = "$service.host.name$" + event_servicename = "$service.name$" + event_author = "$notification.author$" + event_message = "$notification.comment$" + event_object_name = "$host.display_name$: $service.display_name$" + event_extra_tags = serviceExtraTags + } + + vars.event_type = {{`{{ + if (macro("$notification.type$") == "ACKNOWLEDGEMENT") { + return "acknowledgement" + } + + return "" + }}`}} + + vars.event_severity = {{`{{ + if (macro("$notification.type$") != "ACKNOWLEDGEMENT") { + var state = macro("$service.state$") + if (state == "OK") { + return "ok" + } else if (state == "WARNING") { + return "warning" + } else if (state == "CRITICAL") { + return "crit" + } else { // Unknown + return "err" + } + } + + return "" + }}`}} +} + +object EventCommand "icinga-notifications-service-events" use(serviceBody, serviceExtraTags) { + command = [ /* full command line generated from arguments */ ] + + arguments = serviceBody + + vars += { + event_hostname = "$service.host.name$" + event_servicename = "$service.name$" + event_author = "" + event_message = "$service.output$" + event_object_name = "$host.display_name$: $service.display_name$" + event_extra_tags = serviceExtraTags + } + + vars.event_severity = {{`{{ + if (macro("$service.state_type$") == "HARD") { + var state = macro("$service.state$") + if (state == "OK") { + return "ok" + } else if (state == "WARNING") { + return "warning" + } else if (state == "CRITICAL") { + return "crit" + } else { // Unknown + return "err" + } + } + + return "" + }}`}} +} + +template Service "generic-icinga-notifications-service" default { + event_command = "icinga-notifications-service-events" +} + +apply Notification "icinga-notifications-forwarder" to Service { + command = "icinga-notifications-service" + + types = [ Acknowledgement ] + + users = [ "icinga-notifications" ] + + assign where true +} diff --git a/services/icinga_notifications.go b/services/icinga_notifications.go new file mode 100644 index 0000000..dbe200e --- /dev/null +++ b/services/icinga_notifications.go @@ -0,0 +1,50 @@ +package services + +import ( + _ "embed" + "io" + "text/template" +) + +type IcingaNotificationsBase interface { + // Host returns the host on which Icinga Notification's listener can be reached. + Host() string + + // Port return the port on which Icinga Notification's listener can be reached. + Port() string + + // RelationalDatabase returns the instance information of the relational database this instance is using. + RelationalDatabase() RelationalDatabase + + // Cleanup stops the instance and removes everything that was created to start it. + Cleanup() +} + +// IcingaNotifications wraps the IcingaNotificationsBase interface and adds some more helper functions. +type IcingaNotifications struct { + IcingaNotificationsBase + config string +} + +//go:embed icinga_notifications.yml +var icingaNotificationsYmlRawTemplate string +var icingaNotificationsYmlTemplate = template.Must(template.New("icinga_notifications.yml").Parse(icingaNotificationsYmlRawTemplate)) + +func (i IcingaNotifications) WriteConfig(w io.Writer) error { + return icingaNotificationsYmlTemplate.Execute(w, i) +} + +// Config returns additional raw YAML configuration, if any. +func (i IcingaNotifications) Config() string { + return i.config +} + +// IcingaNotificationsOption configures IcingaNotifications. +type IcingaNotificationsOption func(*IcingaNotifications) + +// WithIcingaNotificationsConfig sets additional raw YAML configuration. +func WithIcingaNotificationsConfig(config string) func(notifications *IcingaNotifications) { + return func(db *IcingaNotifications) { + db.config = config + } +} diff --git a/services/icinga_notifications.yml b/services/icinga_notifications.yml new file mode 100644 index 0000000..42f373b --- /dev/null +++ b/services/icinga_notifications.yml @@ -0,0 +1,16 @@ +listen: "{{.Host}}:{{.Port}}" +channel-plugin-dir: /channel + +database: + type: {{.RelationalDatabase.IcingaDbType}} + host: {{.RelationalDatabase.Host}} + port: {{.RelationalDatabase.Port}} + database: {{.RelationalDatabase.Database}} + user: {{.RelationalDatabase.Username}} + password: {{.RelationalDatabase.Password}} + +logging: + level: debug + interval: 1s + +{{.Config}} diff --git a/services/mysql.go b/services/mysql.go index 153e183..865e151 100644 --- a/services/mysql.go +++ b/services/mysql.go @@ -73,3 +73,7 @@ func (m MysqlDatabase) ImportIcingaDbSchema() { } } } + +func (m MysqlDatabase) ImportIcingaNotificationsSchema() { + panic("icinga-notifications does not support MySQL yet") +} diff --git a/services/postgresql.go b/services/postgresql.go index ff85aef..4313bd5 100644 --- a/services/postgresql.go +++ b/services/postgresql.go @@ -59,8 +59,7 @@ func (p PostgresqlDatabase) Open() (*sql.DB, error) { return sql.Open(p.Driver(), p.DSN()) } -func (p PostgresqlDatabase) ImportIcingaDbSchema() { - key := "ICINGA_TESTING_ICINGADB_SCHEMA_PGSQL" +func (p PostgresqlDatabase) importSchema(key string) { schemaFile, ok := os.LookupEnv(key) if !ok { panic(fmt.Errorf("environment variable %s must be set", key)) @@ -68,7 +67,7 @@ func (p PostgresqlDatabase) ImportIcingaDbSchema() { schema, err := os.ReadFile(schemaFile) if err != nil { - panic(fmt.Errorf("failed to read icingadb schema file %q: %w", schemaFile, err)) + panic(fmt.Errorf("failed to read %s schema file %q: %w", key, schemaFile, err)) } db, err := PostgresqlDatabase{PostgresqlDatabaseBase: p}.Open() @@ -79,3 +78,11 @@ func (p PostgresqlDatabase) ImportIcingaDbSchema() { panic(err) } } + +func (p PostgresqlDatabase) ImportIcingaDbSchema() { + p.importSchema("ICINGA_TESTING_ICINGADB_SCHEMA_PGSQL") +} + +func (p PostgresqlDatabase) ImportIcingaNotificationsSchema() { + p.importSchema("ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL") +} diff --git a/services/relational_database.go b/services/relational_database.go index 3793d26..8ebbf1c 100644 --- a/services/relational_database.go +++ b/services/relational_database.go @@ -28,6 +28,9 @@ type RelationalDatabase interface { // ImportIcingaDbSchema imports the Icinga DB schema into this database. ImportIcingaDbSchema() + // ImportIcingaNotificationsSchema imports the Icinga Notifications schema into this database. + ImportIcingaNotificationsSchema() + // Cleanup removes the database. Cleanup() }