~xenrox/ntfy-alertmanager

6c2521eeca045c5e3e9d84c485484fcbb0d637ad — Thorben Günther a month ago 8ea6292
config: Support "include" directive

With this directive other configuration files can be imported into the
main config. Can be useful for keeping secrets out of the latter.

Closes: https://todo.xenrox.net/~xenrox/ntfy-alertmanager/23
2 files changed, 157 insertions(+), 125 deletions(-)

M config.scfg
M config/config.go
M config.scfg => config.scfg +5 -0
@@ 1,3 1,8 @@
# Absolute path to another scfg configuration file which will be included.
# This directive can be specified multiple times in the main configuration, 
# but only the last occurrence of a setting will be used. Settings from
# the main configuration will take precedence.
include /etc/ntfy-alertmanager/ntfy.scfg
# Public facing base URL of the service (e.g. https://ntfy-alertmanager.xenrox.net)
# This setting is required for the "Silence" feature.
base-url https://ntfy-alertmanager.xenrox.net

M config/config.go => config/config.go +152 -125
@@ 21,6 21,7 @@ const (

// Config is the configuration of the bridge.
type Config struct {
	Include     string
	BaseURL     string
	HTTPAddress string
	LogLevel    string


@@ 82,60 83,40 @@ type resolvedConfig struct {
	Priority string
}

// ReadConfig reads an scfg formatted file and returns the configuration struct.
func ReadConfig(path string) (*Config, error) {
	cfg, err := scfg.Load(path)
	if err != nil {
		return nil, err
	}

	config := new(Config)
	// Set default values
	config.HTTPAddress = "127.0.0.1:8080"
	config.LogLevel = "info"
	config.LogFormat = "text"
	config.AlertMode = Multi

	config.Cache.Type = "disabled"
	config.Cache.Duration = time.Hour * 24
	// memory
	config.Cache.CleanupInterval = time.Hour
	// redis
	config.Cache.RedisURL = "redis://localhost:6379"

	d := cfg.Get("log-level")
func parseBlock(block scfg.Block, config *Config) error {
	d := block.Get("log-level")
	if d != nil {
		if err := d.ParseParams(&config.LogLevel); err != nil {
			return nil, err
			return err
		}
	}

	d = cfg.Get("log-format")
	d = block.Get("log-format")
	if d != nil {
		if err := d.ParseParams(&config.LogFormat); err != nil {
			return nil, err
			return err
		}
	}

	d = cfg.Get("http-address")
	d = block.Get("http-address")
	if d != nil {
		if err := d.ParseParams(&config.HTTPAddress); err != nil {
			return nil, err
			return err
		}
	}

	d = cfg.Get("base-url")
	d = block.Get("base-url")
	if d != nil {
		if err := d.ParseParams(&config.BaseURL); err != nil {
			return nil, err
			return err
		}
	}

	d = cfg.Get("alert-mode")
	d = block.Get("alert-mode")
	if d != nil {
		var mode string
		if err := d.ParseParams(&mode); err != nil {
			return nil, err
			return err
		}

		switch strings.ToLower(mode) {


@@ 146,36 127,31 @@ func ReadConfig(path string) (*Config, error) {
			config.AlertMode = Multi

		default:
			return nil, fmt.Errorf("%q directive: illegal mode %q", d.Name, mode)
			return fmt.Errorf("%q directive: illegal mode %q", d.Name, mode)
		}
	}

	d = cfg.Get("user")
	d = block.Get("user")
	if d != nil {
		if err := d.ParseParams(&config.User); err != nil {
			return nil, err
			return err
		}
	}

	d = cfg.Get("password")
	d = block.Get("password")
	if d != nil {
		if err := d.ParseParams(&config.Password); err != nil {
			return nil, err
			return err
		}
	}

	if (config.Password != "" && config.User == "") ||
		(config.Password == "" && config.User != "") {
		return nil, errors.New("user and password have to be set together")
	}

	labelsDir := cfg.Get("labels")
	labelsDir := block.Get("labels")
	if labelsDir != nil {
		d = labelsDir.Children.Get("order")
		if d != nil {
			var order string
			if err := d.ParseParams(&order); err != nil {
				return nil, err
				return err
			}

			config.Labels.Order = strings.Split(order, ",")


@@ 188,13 164,13 @@ func ReadConfig(path string) (*Config, error) {
				var name string

				if err := labelDir.ParseParams(&name); err != nil {
					return nil, err
					return err
				}

				d = labelDir.Children.Get("priority")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Priority); err != nil {
						return nil, err
						return err
					}
				}



@@ 202,7 178,7 @@ func ReadConfig(path string) (*Config, error) {
				if d != nil {
					var tags string
					if err := d.ParseParams(&tags); err != nil {
						return nil, err
						return err
					}

					labelConfig.Tags = strings.Split(tags, ",")


@@ 211,21 187,21 @@ func ReadConfig(path string) (*Config, error) {
				d = labelDir.Children.Get("icon")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Icon); err != nil {
						return nil, err
						return err
					}
				}

				d = labelDir.Children.Get("email-address")
				if d != nil {
					if err := d.ParseParams(&labelConfig.EmailAddress); err != nil {
						return nil, err
						return err
					}
				}

				d = labelDir.Children.Get("call")
				if d != nil {
					if err := d.ParseParams(&labelConfig.Call); err != nil {
						return nil, err
						return err
					}
				}



@@ 236,80 212,67 @@ func ReadConfig(path string) (*Config, error) {
		config.Labels.Label = labels
	}

	ntfyDir := cfg.Get("ntfy")
	if ntfyDir == nil {
		return nil, fmt.Errorf("%q directive missing", "ntfy")
	}

	d = ntfyDir.Children.Get("topic")
	if d == nil {
		return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy")
	}
	if err := d.ParseParams(&config.Ntfy.Topic); err != nil {
		return nil, err
	}

	d = ntfyDir.Children.Get("user")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.User); err != nil {
			return nil, err
	ntfyDir := block.Get("ntfy")
	if ntfyDir != nil {
		d = ntfyDir.Children.Get("topic")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Topic); err != nil {
				return err
			}
		}
	}

	d = ntfyDir.Children.Get("password")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.Password); err != nil {
			return nil, err
		d = ntfyDir.Children.Get("user")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.User); err != nil {
				return err
			}
		}
	}

	if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
		(config.Ntfy.Password == "" && config.Ntfy.User != "") {
		return nil, errors.New("ntfy: user and password have to be set together")
	}
		d = ntfyDir.Children.Get("password")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Password); err != nil {
				return err
			}
		}

	d = ntfyDir.Children.Get("access-token")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
			return nil, err
		d = ntfyDir.Children.Get("access-token")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
				return err
			}
		}
	}

	if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" {
		return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
	}
		d = ntfyDir.Children.Get("certificate-fingerprint")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
				return err
			}

	d = ntfyDir.Children.Get("certificate-fingerprint")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
			return nil, err
			// hex.EncodeToString outputs a lower case string
			config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
		}

		// hex.EncodeToString outputs a lower case string
		config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
	}

	d = ntfyDir.Children.Get("email-address")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
			return nil, err
		d = ntfyDir.Children.Get("email-address")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
				return err
			}
		}
	}

	d = ntfyDir.Children.Get("call")
	if d != nil {
		if err := d.ParseParams(&config.Ntfy.Call); err != nil {
			return nil, err
		d = ntfyDir.Children.Get("call")
		if d != nil {
			if err := d.ParseParams(&config.Ntfy.Call); err != nil {
				return err
			}
		}
	}

	cacheDir := cfg.Get("cache")

	cacheDir := block.Get("cache")
	if cacheDir != nil {
		d = cacheDir.Children.Get("type")
		if d != nil {
			if err := d.ParseParams(&config.Cache.Type); err != nil {
				return nil, err
				return err
			}
		}



@@ 317,12 280,12 @@ func ReadConfig(path string) (*Config, error) {
		d = cacheDir.Children.Get("duration")
		if d != nil {
			if err := d.ParseParams(&durationString); err != nil {
				return nil, err
				return err
			}

			duration, err := time.ParseDuration(durationString)
			if err != nil {
				return nil, err
				return err
			}

			config.Cache.Duration = duration


@@ 333,12 296,12 @@ func ReadConfig(path string) (*Config, error) {
		d = cacheDir.Children.Get("cleanup-interval")
		if d != nil {
			if err := d.ParseParams(&cleanupIntervalString); err != nil {
				return nil, err
				return err
			}

			interval, err := time.ParseDuration(cleanupIntervalString)
			if err != nil {
				return nil, err
				return err
			}

			config.Cache.CleanupInterval = interval


@@ 348,24 311,23 @@ func ReadConfig(path string) (*Config, error) {
		d = cacheDir.Children.Get("redis-url")
		if d != nil {
			if err := d.ParseParams(&config.Cache.RedisURL); err != nil {
				return nil, err
				return err
			}
		}
	}

	amDir := cfg.Get("alertmanager")

	amDir := block.Get("alertmanager")
	if amDir != nil {
		var durationString string
		d = amDir.Children.Get("silence-duration")
		if d != nil {
			if err := d.ParseParams(&durationString); err != nil {
				return nil, err
				return err
			}

			duration, err := time.ParseDuration(durationString)
			if err != nil {
				return nil, err
				return err
			}

			config.Am.SilenceDuration = duration


@@ 374,37 336,32 @@ func ReadConfig(path string) (*Config, error) {
		d = amDir.Children.Get("user")
		if d != nil {
			if err := d.ParseParams(&config.Am.User); err != nil {
				return nil, err
				return err
			}
		}

		d = amDir.Children.Get("password")
		if d != nil {
			if err := d.ParseParams(&config.Am.Password); err != nil {
				return nil, err
				return err
			}
		}

		if (config.Am.Password != "" && config.Am.User == "") ||
			(config.Am.Password == "" && config.Am.User != "") {
			return nil, errors.New("alertmanager: user and password have to be set together")
		}

		d = amDir.Children.Get("url")
		if d != nil {
			if err := d.ParseParams(&config.Am.URL); err != nil {
				return nil, err
				return err
			}
		}
	}

	resolvedDir := cfg.Get("resolved")
	resolvedDir := block.Get("resolved")
	if resolvedDir != nil {
		d = resolvedDir.Children.Get("tags")
		if d != nil {
			var tags string
			if err := d.ParseParams(&tags); err != nil {
				return nil, err
				return err
			}

			config.Resolved.Tags = strings.Split(tags, ",")


@@ 413,17 370,87 @@ func ReadConfig(path string) (*Config, error) {
		d = resolvedDir.Children.Get("icon")
		if d != nil {
			if err := d.ParseParams(&config.Resolved.Icon); err != nil {
				return nil, err
				return err
			}
		}

		d = resolvedDir.Children.Get("priority")
		if d != nil {
			if err := d.ParseParams(&config.Resolved.Priority); err != nil {
				return nil, err
				return err
			}
		}
	}

	return nil
}

// ReadConfig reads an scfg formatted file and returns the configuration struct.
func ReadConfig(path string) (*Config, error) {
	cfg, err := scfg.Load(path)
	if err != nil {
		return nil, err
	}

	config := new(Config)
	// Set default values
	config.HTTPAddress = "127.0.0.1:8080"
	config.LogLevel = "info"
	config.LogFormat = "text"
	config.AlertMode = Multi

	config.Cache.Type = "disabled"
	config.Cache.Duration = time.Hour * 24
	// memory
	config.Cache.CleanupInterval = time.Hour
	// redis
	config.Cache.RedisURL = "redis://localhost:6379"

	includeDirs := cfg.GetAll("include")
	for _, d := range includeDirs {
		var includePath string
		if err := d.ParseParams(&includePath); err != nil {
			return nil, err
		}

		block, err := scfg.Load(includePath)
		if err != nil {
			return nil, fmt.Errorf("cannot load included config file %q: %v", includePath, err)
		}

		if err := parseBlock(block, config); err != nil {
			return nil, fmt.Errorf("cannot parse included config file %q: %v", includePath, err)
		}
	}

	err = parseBlock(cfg, config)
	if err != nil {
		return nil, err
	}

	// Check settings
	if (config.Password != "" && config.User == "") ||
		(config.Password == "" && config.User != "") {
		return nil, errors.New("user and password have to be set together")
	}

	if config.Ntfy.Topic == "" {
		return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy")
	}

	if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
		(config.Ntfy.Password == "" && config.Ntfy.User != "") {
		return nil, errors.New("ntfy: user and password have to be set together")
	}

	if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" {
		return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
	}

	if (config.Am.Password != "" && config.Am.User == "") ||
		(config.Am.Password == "" && config.Am.User != "") {
		return nil, errors.New("alertmanager: user and password have to be set together")
	}

	return config, nil
}