Skip to content

Commit

Permalink
Allow to configure notications credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
yhabteab committed Jul 25, 2024
1 parent 9db0aa6 commit 81821c7
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 17 deletions.
5 changes: 5 additions & 0 deletions cmd/icinga-kubernetes/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/icinga/icinga-kubernetes/pkg/com"
"github.com/icinga/icinga-kubernetes/pkg/database"
"github.com/icinga/icinga-kubernetes/pkg/metrics"
"github.com/icinga/icinga-kubernetes/pkg/notifications"
"github.com/icinga/icinga-kubernetes/pkg/periodic"
schemav1 "github.com/icinga/icinga-kubernetes/pkg/schema/v1"
"github.com/icinga/icinga-kubernetes/pkg/sync"
Expand Down Expand Up @@ -92,6 +93,10 @@ func main() {
}
}

if err := notifications.SyncSourceConfig(context.Background(), db, &cfg.Notifications); err != nil {
klog.Fatal(err)
}

g, ctx := errgroup.WithContext(context.Background())

if cfg.Prometheus.Url != "" {
Expand Down
17 changes: 17 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,20 @@ database:
prometheus:
# Prometheus server URL.
# url: http://localhost:9090

# Configuration for Icinga Notifications daemon.
notifications:
# Icinga Notifications daemon base URL.
# If not set, Icinga for Kubernetes won't send any event to Icinga Notifications (disabled).
# url: http://localhost:5680

# The base URL of your Icinga for Kubernetes web module to be used for the generated Icinga Notifications events.
# kubernetes_web_url: http://localhost/icingaweb2/kubernetes

# The Icinga for Kubernetes source username used to authenticate in Icinga Notifications.
# Leave it empty to let Icinga for Kubernetes generate the credentials automatically.
# username: kubernetes

# The Icinga for Kubernetes source password used to authenticate in Icinga Notifications.
# Leave it empty to let Icinga for Kubernetes generate the credentials automatically.
# password: 0bcf3398-12c8-4cb5-849d-81dcd0644cc7
10 changes: 6 additions & 4 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/logging"
"github.com/icinga/icinga-kubernetes/pkg/metrics"
"github.com/icinga/icinga-kubernetes/pkg/notifications"
)

// Config defines Icinga Kubernetes config.
type Config struct {
Database database.Config `yaml:"database"`
Logging logging.Config `yaml:"logging"`
Prometheus metrics.PrometheusConfig `yaml:"prometheus"`
Database database.Config `yaml:"database"`
Logging logging.Config `yaml:"logging"`
Notifications notifications.Config `yaml:"notifications"`
Prometheus metrics.PrometheusConfig `yaml:"prometheus"`
}

// Validate checks constraints in the supplied configuration and returns an error if they are violated.
Expand All @@ -27,5 +29,5 @@ func (c *Config) Validate() error {
return err
}

return nil
return c.Notifications.Validate()
}
39 changes: 39 additions & 0 deletions pkg/notifications/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package notifications

import (
"github.com/pkg/errors"
"strings"
)

type Config struct {
Url string `yaml:"url"`
Username string `yaml:"username"`
Password string `yaml:"password"`
KubernetesWebUrl string `yaml:"kubernetes_web_url" default:"http://localhost/icingaweb2/kubernetes"`
}

// Validate implements the config.Validator interface.
func (c *Config) Validate() error {
if c.Username == "" && c.Password != "" {
return errors.New("'username' must be set, if password is provided")
}
if c.Username != "" {
// Since Icinga Notifications does not yet support basic HTTP authentication with a simple user and password,
// we have to use a static “username” consisting of `source-` and the actual source ID for the time being.
// See https://github.com/Icinga/icinga-notifications/issues/227
parts := strings.Split(c.Username, "-")
if len(parts) != 2 || parts[0] != "source" {
return errors.New("'username' must be of the form '<source>-<SourceID>'")
}
}
if c.Url == "" && (c.Password != "" || c.Username != "") {
return errors.New("Icinga Notifications base 'url' must be provided, if username and password are set")
}

return nil
}

// IsNotificationsEnabled we are allowed to send events the Icinga Notifications daemon.
func IsNotificationsEnabled(c *Config) bool {
return c.Url != "" && c.Username == ""
}
59 changes: 59 additions & 0 deletions pkg/notifications/notifications.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package notifications

import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/icinga/icinga-kubernetes/pkg/database"
schemav1 "github.com/icinga/icinga-kubernetes/pkg/schema/v1"
"github.com/pkg/errors"
)

// SyncSourceConfig synchronises the Icinga Notifications credentials from the YAML config with the database.
func SyncSourceConfig(ctx context.Context, db *database.Database, config *Config) error {
var typPtr *schemav1.Config

if IsNotificationsEnabled(config) {
var dbConfig []*schemav1.Config
if err := db.SelectContext(ctx, &dbConfig, db.BuildSelectStmt(typPtr, typPtr)); err != nil {
return errors.Wrap(err, "cannot fetch Icinga Notifications credentials from DB")
}

configPairs := []*schemav1.Config{
{schemav1.ConfigKeyNotificationsUsername, "Icinga for Kubernetes"},

Check failure on line 23 in pkg/notifications/notifications.go

View workflow job for this annotation

GitHub Actions / lint

composites: *github.com/icinga/icinga-kubernetes/pkg/schema/v1.Config struct literal uses unkeyed fields (govet)
{schemav1.ConfigKeyNotificationsPassword, uuid.NewString()},

Check failure on line 24 in pkg/notifications/notifications.go

View workflow job for this annotation

GitHub Actions / lint

composites: *github.com/icinga/icinga-kubernetes/pkg/schema/v1.Config struct literal uses unkeyed fields (govet)
}

for _, pair := range configPairs {
for _, dbPair := range dbConfig {
if dbPair.Key == pair.Key {
// If we already have a username and password in the DB, leave them unchanged.
pair.Value = dbPair.Value
break
}
}
}

stmt, _ := db.BuildUpsertStmt(typPtr)
if _, err := db.NamedExecContext(ctx, stmt, configPairs); err != nil {
return errors.Wrap(err, "cannot upsert Icinga Notifications credentials")
}
} else {
stmt := fmt.Sprintf(
"DELETE FROM %s WHERE %[2]s = :password OR %[2]s = :username",
db.QuoteIdentifier(database.TableName(typPtr)),
db.QuoteIdentifier("key"))

// We purposefully do not delete the schemav1.ConfigKeyNotificationsSourceID key as it is used by
// Icinga Notifications Web to delete the actual notification source and afterwards it'll delete it as well.
args := map[string]any{
"password": schemav1.ConfigKeyNotificationsPassword,
"username": schemav1.ConfigKeyNotificationsUsername,
}
if _, err := db.NamedExecContext(ctx, stmt, args); err != nil {
return errors.Wrap(err, "cannot delete Icinga Notifications credentials")
}
}

return nil
}
16 changes: 16 additions & 0 deletions pkg/schema/v1/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package v1

// Config represents a single key => value pair database config entry.
type Config struct {
Key ConfigKey
Value string
}

// ConfigKey represents the database config.Key enums.
type ConfigKey string

const (
ConfigKeyNotificationsSourceID ConfigKey = "notifications.source_id"
ConfigKeyNotificationsUsername ConfigKey = "notifications.username"
ConfigKeyNotificationsPassword ConfigKey = "notifications.password"
)
33 changes: 20 additions & 13 deletions schema/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -851,19 +851,6 @@ CREATE TABLE cron_job_label (
PRIMARY KEY (cron_job_uuid, label_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE kubernetes_schema (
id int unsigned NOT NULL AUTO_INCREMENT,
version varchar(64) NOT NULL,
timestamp bigint unsigned NOT NULL,
success enum('n', 'y') DEFAULT NULL,
reason text DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT idx_kubernetes_schema_version UNIQUE (version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO kubernetes_schema (version, timestamp, success, reason)
VALUES ('0.1.0', UNIX_TIMESTAMP() * 1000, 'y', 'Initial import');

CREATE TABLE prometheus_cluster_metric (
cluster_uuid binary(16) NOT NULL,
timestamp bigint NOT NULL,
Expand Down Expand Up @@ -899,3 +886,23 @@ CREATE TABLE prometheus_container_metric (
value double NOT NULL,
PRIMARY KEY (container_uuid, timestamp, category, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE config (
`key` enum('notifications.username', 'notifications.password', 'notifications.source_id') COLLATE utf8mb4_unicode_ci NOT NULL,
value varchar(255) NOT NULL,

PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE kubernetes_schema (
id int unsigned NOT NULL AUTO_INCREMENT,
version varchar(64) NOT NULL,
timestamp bigint unsigned NOT NULL,
success enum('n', 'y') DEFAULT NULL,
reason text DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT idx_kubernetes_schema_version UNIQUE (version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO kubernetes_schema (version, timestamp, success, reason)
VALUES ('0.1.0', UNIX_TIMESTAMP() * 1000, 'y', 'Initial import');

0 comments on commit 81821c7

Please sign in to comment.