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 17, 2024
1 parent 18da892 commit deedacc
Show file tree
Hide file tree
Showing 11 changed files with 651 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
164 changes: 164 additions & 0 deletions internal/services/notifications/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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"
"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[*dockerInstance]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[*dockerInstance]struct{}),
}
}

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

idb := &services.IcingaNotifications{IcingaNotificationsBase: inst}
services.WithIcingaNotificationsDefaultsEnvConfig(inst.info.rdb, ":"+defaultPort)(idb)
for _, option := range options {
option(idb)
}

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,
Env: idb.ConfEnviron(),
}, &container.HostConfig{
Mounts: []mount.Mount{{
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([]*dockerInstance, 0, len(i.running))
for inst := range i.running {
instances = append(instances, inst)
}
i.runningMutex.Unlock()

for _, inst := range instances {
inst.Cleanup()
}
}

type dockerInstance struct {
info
icingaNotificationsDockerBinary *dockerCreator
logger *zap.Logger
containerId string
}

var _ services.IcingaNotificationsBase = (*dockerInstance)(nil)

func (i *dockerInstance) 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")
}
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
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 deedacc

Please sign in to comment.