Skip to content

Commit 091308c

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 091308c

File tree

12 files changed

+670
-20
lines changed

12 files changed

+670
-20
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: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
"os"
15+
"sync"
16+
"sync/atomic"
17+
)
18+
19+
type dockerCreator struct {
20+
logger *zap.Logger
21+
dockerClient *client.Client
22+
dockerNetworkId string
23+
containerNamePrefix string
24+
sharedDirPath string
25+
containerCounter uint32
26+
27+
runningMutex sync.Mutex
28+
running map[*dockerBinaryInstance]struct{}
29+
}
30+
31+
var _ Creator = (*dockerCreator)(nil)
32+
33+
func NewDockerCreator(
34+
logger *zap.Logger,
35+
dockerClient *client.Client,
36+
containerNamePrefix string,
37+
dockerNetworkId string,
38+
sharedDirPath string,
39+
) Creator {
40+
return &dockerCreator{
41+
logger: logger.With(zap.Bool("icinga_notifications", true)),
42+
dockerClient: dockerClient,
43+
dockerNetworkId: dockerNetworkId,
44+
containerNamePrefix: containerNamePrefix,
45+
sharedDirPath: sharedDirPath,
46+
running: make(map[*dockerBinaryInstance]struct{}),
47+
}
48+
}
49+
50+
func (i *dockerCreator) CreateIcingaNotifications(
51+
rdb services.RelationalDatabase,
52+
options ...services.IcingaNotificationsOption,
53+
) services.IcingaNotificationsBase {
54+
inst := &dockerBinaryInstance{
55+
info: info{
56+
rdb: rdb,
57+
port: defaultPort,
58+
},
59+
logger: i.logger,
60+
icingaNotificationsDockerBinary: i,
61+
}
62+
63+
configFile, err := os.CreateTemp("", "icinga_notifications.yml")
64+
if err != nil {
65+
panic(err)
66+
}
67+
err = configFile.Chmod(0666) // defaults to 0600, might result in being unreadable from container UID 1000
68+
if err != nil {
69+
panic(err)
70+
}
71+
idb := &services.IcingaNotifications{IcingaNotificationsBase: inst}
72+
for _, option := range options {
73+
option(idb)
74+
}
75+
if err = idb.WriteConfig(configFile); err != nil {
76+
panic(err)
77+
}
78+
inst.configFileName = configFile.Name()
79+
err = configFile.Close()
80+
if err != nil {
81+
panic(err)
82+
}
83+
84+
containerName := fmt.Sprintf("%s-%d", i.containerNamePrefix, atomic.AddUint32(&i.containerCounter, 1))
85+
inst.logger = inst.logger.With(zap.String("container-name", containerName))
86+
networkName, err := utils.DockerNetworkName(context.Background(), i.dockerClient, i.dockerNetworkId)
87+
if err != nil {
88+
panic(err)
89+
}
90+
91+
dockerImage := utils.GetEnvDefault("ICINGA_TESTING_NOTIFICATIONS_IMAGE", "icinga-notifications:latest")
92+
err = utils.DockerImagePull(context.Background(), inst.logger, i.dockerClient, dockerImage, false)
93+
if err != nil {
94+
panic(err)
95+
}
96+
97+
cont, err := i.dockerClient.ContainerCreate(context.Background(), &container.Config{
98+
Image: dockerImage,
99+
}, &container.HostConfig{
100+
Mounts: []mount.Mount{{
101+
Type: mount.TypeBind,
102+
Source: inst.configFileName,
103+
Target: "/etc/icinga-notifications/config.yml",
104+
ReadOnly: true,
105+
}, {
106+
Type: mount.TypeBind,
107+
Source: i.sharedDirPath,
108+
Target: "/shared",
109+
ReadOnly: true,
110+
}},
111+
}, &network.NetworkingConfig{
112+
EndpointsConfig: map[string]*network.EndpointSettings{
113+
networkName: {
114+
NetworkID: i.dockerNetworkId,
115+
},
116+
},
117+
}, nil, containerName)
118+
if err != nil {
119+
inst.logger.Fatal("failed to create icinga-notifications container", zap.Error(err))
120+
}
121+
inst.containerId = cont.ID
122+
inst.logger = inst.logger.With(zap.String("container-id", cont.ID))
123+
inst.logger.Debug("created container")
124+
125+
err = utils.ForwardDockerContainerOutput(context.Background(), i.dockerClient, cont.ID,
126+
false, utils.NewLineWriter(func(line []byte) {
127+
inst.logger.Debug("container output",
128+
zap.ByteString("line", line))
129+
}))
130+
if err != nil {
131+
inst.logger.Fatal("failed to attach to container output", zap.Error(err))
132+
}
133+
134+
err = i.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
135+
if err != nil {
136+
inst.logger.Fatal("failed to start container", zap.Error(err))
137+
}
138+
inst.logger.Debug("started container")
139+
140+
inst.info.host = utils.MustString(utils.DockerContainerAddress(context.Background(), i.dockerClient, cont.ID))
141+
142+
i.runningMutex.Lock()
143+
i.running[inst] = struct{}{}
144+
i.runningMutex.Unlock()
145+
146+
return inst
147+
}
148+
149+
func (i *dockerCreator) Cleanup() {
150+
i.runningMutex.Lock()
151+
instances := make([]*dockerBinaryInstance, 0, len(i.running))
152+
for inst := range i.running {
153+
instances = append(instances, inst)
154+
}
155+
i.runningMutex.Unlock()
156+
157+
for _, inst := range instances {
158+
inst.Cleanup()
159+
}
160+
}
161+
162+
type dockerBinaryInstance struct {
163+
info
164+
icingaNotificationsDockerBinary *dockerCreator
165+
logger *zap.Logger
166+
containerId string
167+
configFileName string
168+
}
169+
170+
var _ services.IcingaNotificationsBase = (*dockerBinaryInstance)(nil)
171+
172+
func (i *dockerBinaryInstance) Cleanup() {
173+
i.icingaNotificationsDockerBinary.runningMutex.Lock()
174+
delete(i.icingaNotificationsDockerBinary.running, i)
175+
i.icingaNotificationsDockerBinary.runningMutex.Unlock()
176+
177+
err := i.icingaNotificationsDockerBinary.dockerClient.ContainerRemove(context.Background(), i.containerId, types.ContainerRemoveOptions{
178+
Force: true,
179+
RemoveVolumes: true,
180+
})
181+
if err != nil {
182+
panic(err)
183+
}
184+
i.logger.Debug("removed container")
185+
186+
err = os.Remove(i.configFileName)
187+
if err != nil {
188+
panic(err)
189+
}
190+
}
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: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
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 (
@@ -24,6 +27,7 @@ import (
2427
"github.com/icinga/icinga-testing/internal/services/icinga2"
2528
"github.com/icinga/icinga-testing/internal/services/icingadb"
2629
"github.com/icinga/icinga-testing/internal/services/mysql"
30+
"github.com/icinga/icinga-testing/internal/services/notifications"
2731
"github.com/icinga/icinga-testing/internal/services/postgresql"
2832
"github.com/icinga/icinga-testing/internal/services/redis"
2933
"github.com/icinga/icinga-testing/services"
@@ -50,18 +54,19 @@ import (
5054
// m.Run()
5155
// }
5256
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
57+
mutex sync.Mutex
58+
deferredCleanup []func()
59+
prefix string
60+
dockerClient *client.Client
61+
dockerNetworkId string
62+
mysql mysql.Creator
63+
postgresql postgresql.Creator
64+
redis redis.Creator
65+
icinga2 icinga2.Creator
66+
icingaDb icingadb.Creator
67+
icingaNotifications notifications.Creator
68+
logger *zap.Logger
69+
loggerDebugCore zapcore.Core
6570
}
6671

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

274279
// 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.
278280
func (it *IT) IcingaDbInstance(redis services.RedisServer, rdb services.RelationalDatabase, options ...services.IcingaDbOption) services.IcingaDb {
279281
return services.IcingaDb{IcingaDbBase: it.getIcingaDb().CreateIcingaDb(redis, rdb, options...)}
280282
}
@@ -288,6 +290,42 @@ func (it *IT) IcingaDbInstanceT(
288290
return i
289291
}
290292

293+
func (it *IT) getIcingaNotifications() notifications.Creator {
294+
shareDir, ok := os.LookupEnv("ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR")
295+
if !ok {
296+
panic("environment variable ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR must be set")
297+
}
298+
299+
it.mutex.Lock()
300+
defer it.mutex.Unlock()
301+
302+
if it.icingaNotifications == nil {
303+
it.icingaNotifications = notifications.NewDockerCreator(
304+
it.logger, it.dockerClient, it.prefix+"-icinga-notifications", it.dockerNetworkId, shareDir)
305+
it.deferCleanup(it.icingaNotifications.Cleanup)
306+
}
307+
308+
return it.icingaNotifications
309+
}
310+
311+
// IcingaNotificationsInstance starts a new Icinga Notifications instance.
312+
func (it *IT) IcingaNotificationsInstance(
313+
rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
314+
) services.IcingaNotifications {
315+
return services.IcingaNotifications{
316+
IcingaNotificationsBase: it.getIcingaNotifications().CreateIcingaNotifications(rdb, options...),
317+
}
318+
}
319+
320+
// IcingaNotificationsInstanceT creates a new Icinga Notifications instance and registers its cleanup function with testing.T.
321+
func (it *IT) IcingaNotificationsInstanceT(
322+
t testing.TB, rdb services.RelationalDatabase, options ...services.IcingaNotificationsOption,
323+
) services.IcingaNotifications {
324+
i := it.IcingaNotificationsInstance(rdb, options...)
325+
t.Cleanup(i.Cleanup)
326+
return i
327+
}
328+
291329
// Logger returns a *zap.Logger which additionally logs the current test case name.
292330
func (it *IT) Logger(t testing.TB) *zap.Logger {
293331
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)