Skip to content

Commit bf2ccd4

Browse files
committed
Initial support for Icinga Notifications
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. To receive notifications, IcingaNotificationsWebhookReceiver can be used to launch a host-bound web server to act on Icinga Notification's Webhook channel.
1 parent 18da892 commit bf2ccd4

File tree

14 files changed

+779
-23
lines changed

14 files changed

+779
-23
lines changed

internal/services/icinga2/docker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ func (n *dockerInstance) EnableIcingaDb(redis services.RedisServerBase) {
189189
services.Icinga2{Icinga2Base: n}.WriteIcingaDbConf(redis)
190190
}
191191

192+
func (n *dockerInstance) EnableIcingaNotifications(notis services.IcingaNotificationsBase) {
193+
services.Icinga2{Icinga2Base: n}.WriteIcingaNotificationsConf(notis)
194+
}
195+
192196
func (n *dockerInstance) Cleanup() {
193197
n.icinga2Docker.runningMutex.Lock()
194198
delete(n.icinga2Docker.running, n)

internal/services/icingadb/docker_binary.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/icinga/icinga-testing/services"
1212
"github.com/icinga/icinga-testing/utils"
1313
"go.uber.org/zap"
14-
"io/ioutil"
1514
"os"
1615
"path/filepath"
1716
"sync"
@@ -67,7 +66,7 @@ func (i *dockerBinaryCreator) CreateIcingaDb(
6766
icingaDbDockerBinary: i,
6867
}
6968

70-
configFile, err := ioutil.TempFile("", "icingadb.yml")
69+
configFile, err := os.CreateTemp("", "icingadb.yml")
7170
if err != nil {
7271
panic(err)
7372
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package notifications
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/docker/docker/api/types"
7+
"github.com/docker/docker/api/types/container"
8+
"github.com/docker/docker/api/types/mount"
9+
"github.com/docker/docker/api/types/network"
10+
"github.com/docker/docker/client"
11+
"github.com/icinga/icinga-testing/services"
12+
"github.com/icinga/icinga-testing/utils"
13+
"go.uber.org/zap"
14+
"sync"
15+
"sync/atomic"
16+
)
17+
18+
type dockerCreator struct {
19+
logger *zap.Logger
20+
dockerClient *client.Client
21+
dockerNetworkId string
22+
containerNamePrefix string
23+
sharedDirPath string
24+
containerCounter uint32
25+
26+
runningMutex sync.Mutex
27+
running map[*dockerInstance]struct{}
28+
}
29+
30+
var _ Creator = (*dockerCreator)(nil)
31+
32+
func NewDockerCreator(
33+
logger *zap.Logger,
34+
dockerClient *client.Client,
35+
containerNamePrefix string,
36+
dockerNetworkId string,
37+
sharedDirPath string,
38+
) Creator {
39+
return &dockerCreator{
40+
logger: logger.With(zap.Bool("icinga_notifications", true)),
41+
dockerClient: dockerClient,
42+
dockerNetworkId: dockerNetworkId,
43+
containerNamePrefix: containerNamePrefix,
44+
sharedDirPath: sharedDirPath,
45+
running: make(map[*dockerInstance]struct{}),
46+
}
47+
}
48+
49+
func (i *dockerCreator) CreateIcingaNotifications(
50+
rdb services.RelationalDatabase,
51+
options ...services.IcingaNotificationsOption,
52+
) services.IcingaNotificationsBase {
53+
inst := &dockerInstance{
54+
info: info{
55+
port: defaultPort,
56+
rdb: rdb,
57+
},
58+
logger: i.logger,
59+
icingaNotificationsDocker: i,
60+
}
61+
62+
idb := &services.IcingaNotifications{IcingaNotificationsBase: inst}
63+
services.WithIcingaNotificationsDefaultsEnvConfig(inst.info.rdb, ":"+defaultPort)(idb)
64+
for _, option := range options {
65+
option(idb)
66+
}
67+
68+
containerName := fmt.Sprintf("%s-%d", i.containerNamePrefix, atomic.AddUint32(&i.containerCounter, 1))
69+
inst.logger = inst.logger.With(zap.String("container-name", containerName))
70+
networkName, err := utils.DockerNetworkName(context.Background(), i.dockerClient, i.dockerNetworkId)
71+
if err != nil {
72+
panic(err)
73+
}
74+
75+
dockerImage := utils.GetEnvDefault("ICINGA_TESTING_NOTIFICATIONS_IMAGE", "icinga-notifications:latest")
76+
err = utils.DockerImagePull(context.Background(), inst.logger, i.dockerClient, dockerImage, false)
77+
if err != nil {
78+
panic(err)
79+
}
80+
81+
cont, err := i.dockerClient.ContainerCreate(context.Background(), &container.Config{
82+
Image: dockerImage,
83+
Env: idb.ConfEnviron(),
84+
}, &container.HostConfig{
85+
Mounts: []mount.Mount{{
86+
Type: mount.TypeBind,
87+
Source: i.sharedDirPath,
88+
Target: "/shared",
89+
ReadOnly: true,
90+
}},
91+
}, &network.NetworkingConfig{
92+
EndpointsConfig: map[string]*network.EndpointSettings{
93+
networkName: {
94+
NetworkID: i.dockerNetworkId,
95+
},
96+
},
97+
}, nil, containerName)
98+
if err != nil {
99+
inst.logger.Fatal("failed to create icinga-notifications container", zap.Error(err))
100+
}
101+
inst.containerId = cont.ID
102+
inst.logger = inst.logger.With(zap.String("container-id", cont.ID))
103+
inst.logger.Debug("created container")
104+
105+
err = utils.ForwardDockerContainerOutput(context.Background(), i.dockerClient, cont.ID,
106+
false, utils.NewLineWriter(func(line []byte) {
107+
inst.logger.Debug("container output",
108+
zap.ByteString("line", line))
109+
}))
110+
if err != nil {
111+
inst.logger.Fatal("failed to attach to container output", zap.Error(err))
112+
}
113+
114+
err = i.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
115+
if err != nil {
116+
inst.logger.Fatal("failed to start container", zap.Error(err))
117+
}
118+
inst.logger.Debug("started container")
119+
120+
inst.info.host = utils.MustString(utils.DockerContainerAddress(context.Background(), i.dockerClient, cont.ID))
121+
122+
i.runningMutex.Lock()
123+
i.running[inst] = struct{}{}
124+
i.runningMutex.Unlock()
125+
126+
return inst
127+
}
128+
129+
func (i *dockerCreator) Cleanup() {
130+
i.runningMutex.Lock()
131+
instances := make([]*dockerInstance, 0, len(i.running))
132+
for inst := range i.running {
133+
instances = append(instances, inst)
134+
}
135+
i.runningMutex.Unlock()
136+
137+
for _, inst := range instances {
138+
inst.Cleanup()
139+
}
140+
}
141+
142+
type dockerInstance struct {
143+
info
144+
icingaNotificationsDocker *dockerCreator
145+
logger *zap.Logger
146+
containerId string
147+
}
148+
149+
var _ services.IcingaNotificationsBase = (*dockerInstance)(nil)
150+
151+
func (i *dockerInstance) Cleanup() {
152+
i.icingaNotificationsDocker.runningMutex.Lock()
153+
delete(i.icingaNotificationsDocker.running, i)
154+
i.icingaNotificationsDocker.runningMutex.Unlock()
155+
156+
err := i.icingaNotificationsDocker.dockerClient.ContainerRemove(context.Background(), i.containerId, types.ContainerRemoveOptions{
157+
Force: true,
158+
RemoveVolumes: true,
159+
})
160+
if err != nil {
161+
panic(err)
162+
}
163+
i.logger.Debug("removed container")
164+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package notifications
2+
3+
import (
4+
"github.com/icinga/icinga-testing/services"
5+
)
6+
7+
// defaultPort of the Icinga Notifications Web Listener.
8+
const defaultPort string = "5680"
9+
10+
type Creator interface {
11+
CreateIcingaNotifications(rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption) services.IcingaNotificationsBase
12+
Cleanup()
13+
}
14+
15+
// info provides a partial implementation of the services.IcingaNotificationsBase interface.
16+
type info struct {
17+
host string
18+
port string
19+
20+
rdb services.RelationalDatabase
21+
}
22+
23+
func (i *info) Host() string {
24+
return i.host
25+
}
26+
27+
func (i *info) Port() string {
28+
return i.port
29+
}
30+
31+
func (i *info) RelationalDatabase() services.RelationalDatabase {
32+
return i.rdb
33+
}

it.go

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,32 @@
1313
// must be compiled using CGO_ENABLED=0
1414
// - ICINGA_TESTING_ICINGADB_SCHEMA_MYSQL: Path to the full Icinga DB schema file for MySQL/MariaDB
1515
// - ICINGA_TESTING_ICINGADB_SCHEMA_PGSQL: Path to the full Icinga DB schema file for PostgreSQL
16+
// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR: Shared path between the Icinga Notifications container and the
17+
// host to, e.g., share a fifo for the file channel.
18+
// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL: Path to the full Icinga Notifications PostgreSQL schema file
1619
package icingatesting
1720

1821
import (
1922
"context"
2023
"flag"
2124
"fmt"
25+
"os"
26+
"sync"
27+
"testing"
28+
2229
"github.com/docker/docker/api/types"
2330
"github.com/docker/docker/client"
2431
"github.com/icinga/icinga-testing/internal/services/icinga2"
2532
"github.com/icinga/icinga-testing/internal/services/icingadb"
2633
"github.com/icinga/icinga-testing/internal/services/mysql"
34+
"github.com/icinga/icinga-testing/internal/services/notifications"
2735
"github.com/icinga/icinga-testing/internal/services/postgresql"
2836
"github.com/icinga/icinga-testing/internal/services/redis"
2937
"github.com/icinga/icinga-testing/services"
3038
"github.com/icinga/icinga-testing/utils"
3139
"go.uber.org/zap"
3240
"go.uber.org/zap/zapcore"
3341
"go.uber.org/zap/zaptest"
34-
"os"
35-
"sync"
36-
"testing"
3742
)
3843

3944
// IT is the core type to start interacting with this module.
@@ -50,18 +55,20 @@ import (
5055
// m.Run()
5156
// }
5257
type IT struct {
53-
mutex sync.Mutex
54-
deferredCleanup []func()
55-
prefix string
56-
dockerClient *client.Client
57-
dockerNetworkId string
58-
mysql mysql.Creator
59-
postgresql postgresql.Creator
60-
redis redis.Creator
61-
icinga2 icinga2.Creator
62-
icingaDb icingadb.Creator
63-
logger *zap.Logger
64-
loggerDebugCore zapcore.Core
58+
mutex sync.Mutex
59+
deferredCleanup []func()
60+
prefix string
61+
dockerClient *client.Client
62+
dockerNetworkId string
63+
mysql mysql.Creator
64+
postgresql postgresql.Creator
65+
redis redis.Creator
66+
icinga2 icinga2.Creator
67+
icingaDb icingadb.Creator
68+
icingaNotifications notifications.Creator
69+
icingaNotificationsWebhookReceiver *services.IcingaNotificationsWebhookReceiver
70+
logger *zap.Logger
71+
loggerDebugCore zapcore.Core
6572
}
6673

6774
var flagDebugLog = flag.String("icingatesting.debuglog", "", "file to write debug log to")
@@ -272,9 +279,6 @@ func (it *IT) getIcingaDb() icingadb.Creator {
272279
}
273280

274281
// IcingaDbInstance starts a new Icinga DB instance.
275-
//
276-
// It expects the ICINGA_TESTING_ICINGADB_BINARY environment variable to be set to the path of a precompiled icingadb
277-
// binary which is then started in a new Docker container when this function is called.
278282
func (it *IT) IcingaDbInstance(redis services.RedisServer, rdb services.RelationalDatabase, options ...services.IcingaDbOption) services.IcingaDb {
279283
return services.IcingaDb{IcingaDbBase: it.getIcingaDb().CreateIcingaDb(redis, rdb, options...)}
280284
}
@@ -288,6 +292,76 @@ func (it *IT) IcingaDbInstanceT(
288292
return i
289293
}
290294

295+
func (it *IT) getIcingaNotifications() notifications.Creator {
296+
shareDir, ok := os.LookupEnv("ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR")
297+
if !ok {
298+
panic("environment variable ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR must be set")
299+
}
300+
301+
it.mutex.Lock()
302+
defer it.mutex.Unlock()
303+
304+
if it.icingaNotifications == nil {
305+
it.icingaNotifications = notifications.NewDockerCreator(
306+
it.logger, it.dockerClient, it.prefix+"-icinga-notifications", it.dockerNetworkId, shareDir)
307+
it.deferCleanup(it.icingaNotifications.Cleanup)
308+
}
309+
310+
return it.icingaNotifications
311+
}
312+
313+
// IcingaNotificationsInstance starts a new Icinga Notifications instance.
314+
func (it *IT) IcingaNotificationsInstance(
315+
rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
316+
) services.IcingaNotifications {
317+
return services.IcingaNotifications{
318+
IcingaNotificationsBase: it.getIcingaNotifications().CreateIcingaNotifications(rdb, options...),
319+
}
320+
}
321+
322+
// IcingaNotificationsInstanceT creates a new Icinga Notifications instance and registers its cleanup function with testing.T.
323+
func (it *IT) IcingaNotificationsInstanceT(
324+
t testing.TB, rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
325+
) services.IcingaNotifications {
326+
i := it.IcingaNotificationsInstance(rdb, options...)
327+
t.Cleanup(i.Cleanup)
328+
return i
329+
}
330+
331+
func (it *IT) getIcingaNotificationsWebhookReceiver() *services.IcingaNotificationsWebhookReceiver {
332+
it.mutex.Lock()
333+
defer it.mutex.Unlock()
334+
335+
if it.icingaNotificationsWebhookReceiver == nil {
336+
networkHost, err := utils.DockerNetworkHostAddress(context.Background(), it.dockerClient, it.dockerNetworkId)
337+
if err != nil {
338+
it.logger.Fatal("cannot get docker host address", zap.Error(err))
339+
}
340+
port, err := utils.OpenTcpPort()
341+
if err != nil {
342+
it.logger.Fatal("cannot get an open TCP port", zap.Error(err))
343+
}
344+
345+
webhookRec, err := services.LaunchIcingaNotificationsWebhookReceiver(fmt.Sprintf("%s:%d", networkHost, port))
346+
if err != nil {
347+
it.logger.Fatal("cannot launch Icinga Notifications webhook receiver", zap.Error(err))
348+
}
349+
350+
it.icingaNotificationsWebhookReceiver = webhookRec
351+
it.deferCleanup(it.icingaNotificationsWebhookReceiver.Cleanup)
352+
}
353+
354+
return it.icingaNotificationsWebhookReceiver
355+
}
356+
357+
// IcingaNotificationsWebhookReceiverInstanceT creates a new Icinga Notifications Webhook Receiver instance and
358+
// registers its cleanup function with testing.T.
359+
func (it *IT) IcingaNotificationsWebhookReceiverInstanceT(t testing.TB) *services.IcingaNotificationsWebhookReceiver {
360+
webhookRec := it.getIcingaNotificationsWebhookReceiver()
361+
t.Cleanup(webhookRec.Cleanup)
362+
return webhookRec
363+
}
364+
291365
// Logger returns a *zap.Logger which additionally logs the current test case name.
292366
func (it *IT) Logger(t testing.TB) *zap.Logger {
293367
cores := []zapcore.Core{zaptest.NewLogger(t, zaptest.WrapOptions(zap.IncreaseLevel(zap.InfoLevel))).Core()}

0 commit comments

Comments
 (0)