Skip to content

Commit

Permalink
Initial support for Icinga Notifications
Browse files Browse the repository at this point in the history
Inspired by the existing code for the Icinga DB, support for Icinga
Notifications was added. Thus, there might be some level of code
duplication between those two.

The custom Icinga 2 configuration was sourced from the Icinga
Notifications repository, but edited to not being parsed as a faulty Go
template.
  • Loading branch information
oxzi committed Jan 3, 2024
1 parent 07586db commit 091308c
Show file tree
Hide file tree
Showing 12 changed files with 670 additions and 20 deletions.
4 changes: 4 additions & 0 deletions internal/services/icinga2/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions internal/services/icingadb/docker_binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
190 changes: 190 additions & 0 deletions internal/services/notifications/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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/icinga/icinga-testing/services"
"github.com/icinga/icinga-testing/utils"
"go.uber.org/zap"
"os"
"sync"
"sync/atomic"
)

type dockerCreator struct {
logger *zap.Logger
dockerClient *client.Client
dockerNetworkId string
containerNamePrefix string
sharedDirPath string
containerCounter uint32

runningMutex sync.Mutex
running map[*dockerBinaryInstance]struct{}
}

var _ Creator = (*dockerCreator)(nil)

func NewDockerCreator(
logger *zap.Logger,
dockerClient *client.Client,
containerNamePrefix string,
dockerNetworkId string,
sharedDirPath string,
) Creator {
return &dockerCreator{
logger: logger.With(zap.Bool("icinga_notifications", true)),
dockerClient: dockerClient,
dockerNetworkId: dockerNetworkId,
containerNamePrefix: containerNamePrefix,
sharedDirPath: sharedDirPath,
running: make(map[*dockerBinaryInstance]struct{}),
}
}

func (i *dockerCreator) CreateIcingaNotifications(
rdb services.RelationalDatabase,
options ...services.IcingaNotificationsOption,
) services.IcingaNotificationsBase {
inst := &dockerBinaryInstance{
info: info{
rdb: rdb,
port: defaultPort,
},
logger: i.logger,
icingaNotificationsDockerBinary: i,
}

configFile, err := os.CreateTemp("", "icinga_notifications.yml")
if err != nil {
panic(err)
}
err = configFile.Chmod(0666) // defaults to 0600, might result in being unreadable from container UID 1000
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 := utils.GetEnvDefault("ICINGA_TESTING_NOTIFICATIONS_IMAGE", "icinga-notifications: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{
Image: dockerImage,
}, &container.HostConfig{
Mounts: []mount.Mount{{
Type: mount.TypeBind,
Source: inst.configFileName,
Target: "/etc/icinga-notifications/config.yml",
ReadOnly: true,
}, {
Type: mount.TypeBind,
Source: i.sharedDirPath,
Target: "/shared",
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))

i.runningMutex.Lock()
i.running[inst] = struct{}{}
i.runningMutex.Unlock()

return inst
}

func (i *dockerCreator) 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 *dockerCreator
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)
}
}
33 changes: 33 additions & 0 deletions internal/services/notifications/notifications.go
Original file line number Diff line number Diff line change
@@ -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
}
68 changes: 53 additions & 15 deletions it.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
// 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_SHARED_DIR: Shared path between the Icinga Notifications container and the
// host to, e.g., share a fifo for the file channel.
// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL: Path to the full Icinga Notifications PostgreSQL schema file
package icingatesting

import (
Expand All @@ -24,6 +27,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"
Expand All @@ -50,18 +54,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")
Expand Down Expand Up @@ -272,9 +277,6 @@ func (it *IT) getIcingaDb() icingadb.Creator {
}

// IcingaDbInstance starts a new Icinga DB instance.
//
// It expects the ICINGA_TESTING_ICINGADB_BINARY environment variable to be set to the path of a precompiled icingadb
// binary which is then started in a new Docker container when this function is called.
func (it *IT) IcingaDbInstance(redis services.RedisServer, rdb services.RelationalDatabase, options ...services.IcingaDbOption) services.IcingaDb {
return services.IcingaDb{IcingaDbBase: it.getIcingaDb().CreateIcingaDb(redis, rdb, options...)}
}
Expand All @@ -288,6 +290,42 @@ func (it *IT) IcingaDbInstanceT(
return i
}

func (it *IT) getIcingaNotifications() notifications.Creator {
shareDir, ok := os.LookupEnv("ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR")
if !ok {
panic("environment variable ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR must be set")
}

it.mutex.Lock()
defer it.mutex.Unlock()

if it.icingaNotifications == nil {
it.icingaNotifications = notifications.NewDockerCreator(
it.logger, it.dockerClient, it.prefix+"-icinga-notifications", it.dockerNetworkId, shareDir)
it.deferCleanup(it.icingaNotifications.Cleanup)
}

return it.icingaNotifications
}

// IcingaNotificationsInstance starts a new Icinga Notifications instance.
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()}
Expand Down
16 changes: 16 additions & 0 deletions services/icinga2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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())
}
Loading

0 comments on commit 091308c

Please sign in to comment.