Skip to content

Commit 8f5c4c4

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.
1 parent 07586db commit 8f5c4c4

File tree

12 files changed

+697
-17
lines changed

12 files changed

+697
-17
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: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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/docker/go-connections/nat"
12+
"github.com/icinga/icinga-testing/services"
13+
"github.com/icinga/icinga-testing/utils"
14+
"go.uber.org/zap"
15+
"os"
16+
"path/filepath"
17+
"sync"
18+
"sync/atomic"
19+
)
20+
21+
type dockerBinaryCreator struct {
22+
logger *zap.Logger
23+
dockerClient *client.Client
24+
dockerNetworkId string
25+
containerNamePrefix string
26+
binaryPath string
27+
channelDirPath string
28+
containerCounter uint32
29+
30+
runningMutex sync.Mutex
31+
running map[*dockerBinaryInstance]struct{}
32+
}
33+
34+
var _ Creator = (*dockerBinaryCreator)(nil)
35+
36+
func NewDockerBinaryCreator(
37+
logger *zap.Logger,
38+
dockerClient *client.Client,
39+
containerNamePrefix string,
40+
dockerNetworkId string,
41+
binaryPath string,
42+
channelDirPath string,
43+
) Creator {
44+
binaryPath, err := filepath.Abs(binaryPath)
45+
if err != nil {
46+
panic(err)
47+
}
48+
return &dockerBinaryCreator{
49+
logger: logger.With(zap.Bool("icinga_notifications", true)),
50+
dockerClient: dockerClient,
51+
dockerNetworkId: dockerNetworkId,
52+
containerNamePrefix: containerNamePrefix,
53+
binaryPath: binaryPath,
54+
channelDirPath: channelDirPath,
55+
running: make(map[*dockerBinaryInstance]struct{}),
56+
}
57+
}
58+
59+
func (i *dockerBinaryCreator) CreateIcingaNotifications(
60+
rdb services.RelationalDatabase,
61+
options ...services.IcingaNotificationsOption,
62+
) services.IcingaNotificationsBase {
63+
inst := &dockerBinaryInstance{
64+
info: info{
65+
rdb: rdb,
66+
port: defaultPort,
67+
},
68+
logger: i.logger,
69+
icingaNotificationsDockerBinary: i,
70+
}
71+
72+
configFile, err := os.CreateTemp("", "icinga_notifications.yml")
73+
if err != nil {
74+
panic(err)
75+
}
76+
idb := &services.IcingaNotifications{IcingaNotificationsBase: inst}
77+
for _, option := range options {
78+
option(idb)
79+
}
80+
if err = idb.WriteConfig(configFile); err != nil {
81+
panic(err)
82+
}
83+
inst.configFileName = configFile.Name()
84+
err = configFile.Close()
85+
if err != nil {
86+
panic(err)
87+
}
88+
89+
containerName := fmt.Sprintf("%s-%d", i.containerNamePrefix, atomic.AddUint32(&i.containerCounter, 1))
90+
inst.logger = inst.logger.With(zap.String("container-name", containerName))
91+
networkName, err := utils.DockerNetworkName(context.Background(), i.dockerClient, i.dockerNetworkId)
92+
if err != nil {
93+
panic(err)
94+
}
95+
96+
dockerImage := "alpine:latest"
97+
err = utils.DockerImagePull(context.Background(), inst.logger, i.dockerClient, dockerImage, false)
98+
if err != nil {
99+
panic(err)
100+
}
101+
102+
cont, err := i.dockerClient.ContainerCreate(context.Background(), &container.Config{
103+
Cmd: []string{"/icinga-notifications-daemon", "-config", "/icinga_notifications.yml"},
104+
Image: dockerImage,
105+
ExposedPorts: map[nat.Port]struct{}{nat.Port(defaultPort + "/tcp"): {}},
106+
}, &container.HostConfig{
107+
Mounts: []mount.Mount{{
108+
Type: mount.TypeBind,
109+
Source: i.binaryPath,
110+
Target: "/icinga-notifications-daemon",
111+
ReadOnly: true,
112+
}, {
113+
Type: mount.TypeBind,
114+
Source: i.channelDirPath,
115+
Target: "/channel",
116+
ReadOnly: true,
117+
}, {
118+
Type: mount.TypeBind,
119+
Source: inst.configFileName,
120+
Target: "/icinga_notifications.yml",
121+
ReadOnly: true,
122+
}},
123+
}, &network.NetworkingConfig{
124+
EndpointsConfig: map[string]*network.EndpointSettings{
125+
networkName: {
126+
NetworkID: i.dockerNetworkId,
127+
},
128+
},
129+
}, nil, containerName)
130+
if err != nil {
131+
inst.logger.Fatal("failed to create icinga-notifications container", zap.Error(err))
132+
}
133+
inst.containerId = cont.ID
134+
inst.logger = inst.logger.With(zap.String("container-id", cont.ID))
135+
inst.logger.Debug("created container")
136+
137+
err = utils.ForwardDockerContainerOutput(context.Background(), i.dockerClient, cont.ID,
138+
false, utils.NewLineWriter(func(line []byte) {
139+
inst.logger.Debug("container output",
140+
zap.ByteString("line", line))
141+
}))
142+
if err != nil {
143+
inst.logger.Fatal("failed to attach to container output", zap.Error(err))
144+
}
145+
146+
err = i.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
147+
if err != nil {
148+
inst.logger.Fatal("failed to start container", zap.Error(err))
149+
}
150+
inst.logger.Debug("started container")
151+
152+
inst.info.host = utils.MustString(utils.DockerContainerAddress(context.Background(), i.dockerClient, cont.ID))
153+
154+
i.runningMutex.Lock()
155+
i.running[inst] = struct{}{}
156+
i.runningMutex.Unlock()
157+
158+
return inst
159+
}
160+
161+
func (i *dockerBinaryCreator) Cleanup() {
162+
i.runningMutex.Lock()
163+
instances := make([]*dockerBinaryInstance, 0, len(i.running))
164+
for inst := range i.running {
165+
instances = append(instances, inst)
166+
}
167+
i.runningMutex.Unlock()
168+
169+
for _, inst := range instances {
170+
inst.Cleanup()
171+
}
172+
}
173+
174+
type dockerBinaryInstance struct {
175+
info
176+
icingaNotificationsDockerBinary *dockerBinaryCreator
177+
logger *zap.Logger
178+
containerId string
179+
configFileName string
180+
}
181+
182+
var _ services.IcingaNotificationsBase = (*dockerBinaryInstance)(nil)
183+
184+
func (i *dockerBinaryInstance) Cleanup() {
185+
i.icingaNotificationsDockerBinary.runningMutex.Lock()
186+
delete(i.icingaNotificationsDockerBinary.running, i)
187+
i.icingaNotificationsDockerBinary.runningMutex.Unlock()
188+
189+
err := i.icingaNotificationsDockerBinary.dockerClient.ContainerRemove(context.Background(), i.containerId, types.ContainerRemoveOptions{
190+
Force: true,
191+
RemoveVolumes: true,
192+
})
193+
if err != nil {
194+
panic(err)
195+
}
196+
i.logger.Debug("removed container")
197+
198+
err = os.Remove(i.configFileName)
199+
if err != nil {
200+
panic(err)
201+
}
202+
}
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: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
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_BINARY: Path to the Icinga Notifications binary to test. It will run in a
17+
// container and therefore must be compiled using CGO_ENABLED=0
18+
// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR: Path to the Icinga Notifications channel binary directory. Those
19+
// are also needed to be compiled with CGO_ENABLED=0.
20+
// - ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL: Path to the full Icinga Notifications PostgreSQL schema file
1621
package icingatesting
1722

1823
import (
@@ -24,6 +29,7 @@ import (
2429
"github.com/icinga/icinga-testing/internal/services/icinga2"
2530
"github.com/icinga/icinga-testing/internal/services/icingadb"
2631
"github.com/icinga/icinga-testing/internal/services/mysql"
32+
"github.com/icinga/icinga-testing/internal/services/notifications"
2733
"github.com/icinga/icinga-testing/internal/services/postgresql"
2834
"github.com/icinga/icinga-testing/internal/services/redis"
2935
"github.com/icinga/icinga-testing/services"
@@ -50,18 +56,19 @@ import (
5056
// m.Run()
5157
// }
5258
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
59+
mutex sync.Mutex
60+
deferredCleanup []func()
61+
prefix string
62+
dockerClient *client.Client
63+
dockerNetworkId string
64+
mysql mysql.Creator
65+
postgresql postgresql.Creator
66+
redis redis.Creator
67+
icinga2 icinga2.Creator
68+
icingaDb icingadb.Creator
69+
icingaNotifications notifications.Creator
70+
logger *zap.Logger
71+
loggerDebugCore zapcore.Core
6572
}
6673

6774
var flagDebugLog = flag.String("icingatesting.debuglog", "", "file to write debug log to")
@@ -288,6 +295,55 @@ func (it *IT) IcingaDbInstanceT(
288295
return i
289296
}
290297

298+
func (it *IT) getIcingaNotifications() notifications.Creator {
299+
keys := map[string]string{
300+
"ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY": "",
301+
"ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR": "",
302+
}
303+
304+
for key := range keys {
305+
var ok bool
306+
keys[key], ok = os.LookupEnv(key)
307+
if !ok {
308+
panic(fmt.Errorf("environment variable %s must be set", key))
309+
}
310+
}
311+
312+
it.mutex.Lock()
313+
defer it.mutex.Unlock()
314+
315+
if it.icingaNotifications == nil {
316+
it.icingaNotifications = notifications.NewDockerBinaryCreator(
317+
it.logger, it.dockerClient, it.prefix+"-icinga-notifications", it.dockerNetworkId,
318+
keys["ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY"], keys["ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR"])
319+
it.deferCleanup(it.icingaNotifications.Cleanup)
320+
}
321+
322+
return it.icingaNotifications
323+
}
324+
325+
// IcingaNotificationsInstance starts a new Icinga Notifications instance.
326+
//
327+
// It expects the ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY environment variable to be set to the path of a precompiled
328+
// binary which is then started in a new Docker container when this function is called. The environment variable
329+
// ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR needs also to be set to a directory holding the channel binaries.
330+
func (it *IT) IcingaNotificationsInstance(
331+
rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
332+
) services.IcingaNotifications {
333+
return services.IcingaNotifications{
334+
IcingaNotificationsBase: it.getIcingaNotifications().CreateIcingaNotifications(rdb, options...),
335+
}
336+
}
337+
338+
// IcingaNotificationsInstanceT creates a new Icinga Notifications instance and registers its cleanup function with testing.T.
339+
func (it *IT) IcingaNotificationsInstanceT(
340+
t testing.TB, rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
341+
) services.IcingaNotifications {
342+
i := it.IcingaNotificationsInstance(rdb, options...)
343+
t.Cleanup(i.Cleanup)
344+
return i
345+
}
346+
291347
// Logger returns a *zap.Logger which additionally logs the current test case name.
292348
func (it *IT) Logger(t testing.TB) *zap.Logger {
293349
cores := []zapcore.Core{zaptest.NewLogger(t, zaptest.WrapOptions(zap.IncreaseLevel(zap.InfoLevel))).Core()}

services/icinga2.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type Icinga2Base interface {
4040
// EnableIcingaDb enables the icingadb feature on this node using the connection details of redis.
4141
EnableIcingaDb(redis RedisServerBase)
4242

43+
// EnableIcingaNotifications enables the Icinga Notifications integration with the custom configuration.
44+
EnableIcingaNotifications(IcingaNotificationsBase)
45+
4346
// Cleanup stops the node and removes everything that was created to start this node.
4447
Cleanup()
4548
}
@@ -128,3 +131,16 @@ func (i Icinga2) WriteIcingaDbConf(r RedisServerBase) {
128131
}
129132
i.WriteConfig(fmt.Sprintf("etc/icinga2/features-enabled/icingadb_%s_%s.conf", r.Host(), r.Port()), b.Bytes())
130133
}
134+
135+
//go:embed icinga2_icinga_notifications.conf
136+
var icinga2IcingaNotificationsConfRawTemplate string
137+
var icinga2IcingaNotificationsConfTemplate = template.Must(template.New("icinga-notifications.conf").Parse(icinga2IcingaNotificationsConfRawTemplate))
138+
139+
func (i Icinga2) WriteIcingaNotificationsConf(notis IcingaNotificationsBase) {
140+
b := bytes.NewBuffer(nil)
141+
err := icinga2IcingaNotificationsConfTemplate.Execute(b, notis)
142+
if err != nil {
143+
panic(err)
144+
}
145+
i.WriteConfig("etc/icinga2/features-enabled/icinga_notifications.conf", b.Bytes())
146+
}

0 commit comments

Comments
 (0)