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 15, 2024
1 parent 18da892 commit 9cf73ad
Show file tree
Hide file tree
Showing 12 changed files with 674 additions and 23 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
}
75 changes: 57 additions & 18 deletions it.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@
// 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 (
"context"
"flag"
"fmt"
"os"
"sync"
"testing"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"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"
"github.com/icinga/icinga-testing/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
"os"
"sync"
"testing"
)

// IT is the core type to start interacting with this module.
Expand All @@ -50,18 +55,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 +278,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 +291,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
Loading

0 comments on commit 9cf73ad

Please sign in to comment.