Untitled
unknown
plain_text
2 years ago
8.7 kB
12
Indexable
package config
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/sauron-platform/network-monitor/pkg/defaults"
"github.com/spf13/viper"
"net"
"os"
"reflect"
"regexp"
ctrl "sigs.k8s.io/controller-runtime"
"strconv"
"strings"
"time"
)
var (
log = ctrl.Log.WithName("config")
)
// Configuration options
const (
optionHostname = "hostname"
optionDeviceType = "deviceType"
optionGrpcUntrusted = "grpcUntrusted"
optionGrpcServerPort = "grpcServerPort"
optionLinkServicePort = "linkServicePort"
optionE2EServicePort = "e2eServicePort"
optionNeighServicePort = "neighServicePort"
optionOtelExportEndpoint = "otelConfig.exportEndpoint"
optionOtelExportInterval = "otelConfig.exportInterval"
optionLinkTestDscpValues = "linkTestConfig.dscpValues"
optionLinkTestPayloadLen = "linkTestConfig.payloadLen"
optionLinkTestMeasureInterval = "linkTestConfig.measureInterval"
)
var (
options = [...]string{
optionHostname,
optionDeviceType,
optionGrpcUntrusted,
optionGrpcServerPort,
optionLinkServicePort,
optionE2EServicePort,
optionNeighServicePort,
optionOtelExportEndpoint,
optionOtelExportInterval,
optionLinkTestDscpValues,
optionLinkTestPayloadLen,
optionLinkTestMeasureInterval,
}
)
// Notice: mapstructure tag names must match option definitions.
type OtelConfig struct {
ExportEndpoint string `mapstructure:"exportEndpoint" validate:"isdefault|endpoint_port"`
ExportInterval time.Duration `mapstructure:"exportInterval" validate:"gte=5m"`
}
type LinkTestConfig struct {
DscpValues []uint8 `mapstructure:"dscpValues" validate:"required,dive,gte=0,lte=63"`
PayloadLen uint16 `mapstructure:"payloadLen" validate:"gte=0,lte=8956"`
MeasureInterval time.Duration `mapstructure:"measureInterval" validate:"gte=5s"`
}
type Configuration struct {
Hostname string `mapstructure:"hostname" validate=:"required"`
DeviceType string `mapstructure:"deviceType" validate:"oneof=vCU NGDU"`
GrpcUntrusted bool `mapstructure:"grpcUntrusted" validate:"-"`
GrpcServerPort uint16 `mapstructure:"grpcServerPort" validate:"gte=1,lte=65535"`
LinkServicePort uint16 `mapstructure:"linkServicePort" validate:"gte=1,lte=65535"`
E2EServicePort uint16 `mapstructure:"e2eServicePort" validate:"gte=1,lte=65535"`
NeighServicePort uint16 `mapstructure:"neighServicePort" validate:"gte=1,lte=65535"`
OtelConfig OtelConfig `mapstructure:"otelConfig"`
LinkTestConfig LinkTestConfig `mapstructure:"linkTestConfig"`
}
var Config = &Configuration{}
// Populate populates the fields of the Configuration object taking values from configuration file or environment
// variables.
func (c *Configuration) Populate() error {
var err error
// Retrieve and set up a new viper instance for loading and parsing
v := viper.GetViper()
if err = setupViper(v); err != nil {
return fmt.Errorf("cannot setup viper: %v", err)
}
// Load configuration from configuration file. If the configuration file is not present, fallback to environment
// variables
if err = v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return fmt.Errorf("cannot read config file: %v", err)
}
log.V(1).Info("Cannot find configuration file. Fallback to environment variable")
}
// Set hostname option to machine hostname if it is not provided by the user
if !v.IsSet(optionHostname) {
log.V(1).Info("Hostname option not provided. Attempt to get machine hostname")
// Get hostname of the machine
var hostname string
if hostname, err = os.Hostname(); err != nil {
return fmt.Errorf("cannot retrieve machine hostname: %v", err)
}
log.V(1).Info("Hostname defaulted to machine hostname", "hostname", hostname)
v.SetDefault(optionHostname, hostname)
}
// Parse configuration
if err = v.Unmarshal(c); err != nil {
return fmt.Errorf("cannot parse retrieved configuration: %v", err)
}
// Register custom validations and validate configuration
validate := validator.New()
if err = registerValidations(validate); err != nil {
return err
}
if err = validate.Struct(c); err != nil {
return fmt.Errorf("failed validation: %v", err)
}
return nil
}
const (
validationTagEndpointPort = "endpoint_port"
)
// registerValidations registers custom validations
func registerValidations(validate *validator.Validate) error {
if err := validate.RegisterValidation(validationTagEndpointPort, validateEndpointPort); err != nil {
return fmt.Errorf("cannot register validatation: %v", err)
}
return nil
}
// Regex copied from https://github.com/go-playground/validator repo.
const hostnameRegexStringRFC1123 = `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?$`
var hostnameRegexRFC1123 = regexp.MustCompile(hostnameRegexStringRFC1123)
// validateEndpointPort flags a field as valid if it is the form [host]:port. If host is present, it must be a valid
// hostname (RFC1123) or a valid IP address. Valid IP addresses can be IPv4 or IPv6. IPv6 addresses must be specified
// inside square brackets.
func validateEndpointPort(fl validator.FieldLevel) bool {
field := fl.Field()
if field.Kind() != reflect.String {
return false
}
return isEndpointPort(field.String())
}
func isEndpointPort(val string) bool {
host, port, err := net.SplitHostPort(val)
if err != nil {
return false
}
// Verify if port is a number in the valid range
if portNum, err := strconv.ParseInt(port, 10, 32); err != nil || portNum > 65535 || portNum < 1 {
return false
}
// If host is specified, it should match a DNS name or being an IP address
if host != "" {
return hostnameRegexRFC1123.MatchString(host) || net.ParseIP(host) != nil
}
return true
}
// Default viper configuration parameters.
const (
viperConfigName = "config"
viperConfigType = "yaml"
viperConfigPath = "/config/"
)
// setupViper initializes the viper instance configuration, sets up the options default values and binds each option to
// the corresponding environment variable name.
func setupViper(v *viper.Viper) error {
var err error
// Initialize viper configuration
v.SetConfigName(viperConfigName)
v.SetConfigType(viperConfigType)
v.AddConfigPath(viperConfigPath)
// Set options default values
setOptionsDefaults(v)
// Bind each option to the corresponding environment variable name
if err = bindEnvs(v); err != nil {
return fmt.Errorf("error during environment variable binding: %v", err)
}
//v.OnConfigChange(func(e fsnotify.Event) {
// // TODO:
// fmt.Println("Config file changed:", e.Name)
//})
//v.WatchConfig()
return nil
}
// setOptionsDefaults sets, for each option, the corresponding default value.
func setOptionsDefaults(v *viper.Viper) {
// Default for hostname option is not set here since we don't want to retrieve the os hostname in advance. Moreover,
// retrieval of the hostname could produce an error.
//v.SetDefault(optionHostname, ...)
v.SetDefault(optionDeviceType, defaults.DeviceType)
v.SetDefault(optionGrpcUntrusted, defaults.GrpcUntrusted)
v.SetDefault(optionGrpcServerPort, defaults.GrpcServerPort)
v.SetDefault(optionLinkServicePort, defaults.LinkServicePort)
v.SetDefault(optionE2EServicePort, defaults.E2EServicePort)
v.SetDefault(optionNeighServicePort, defaults.NeighServicePort)
// no default for optionOtelExportEndpoint
v.SetDefault(optionOtelExportInterval, defaults.OtelExportInterval)
v.SetDefault(optionLinkTestDscpValues, defaults.LinkTestDscpValues())
v.SetDefault(optionLinkTestPayloadLen, defaults.LinkTestPayloadLen)
v.SetDefault(optionLinkTestMeasureInterval, defaults.LinkTestMeasureInterval)
}
// bindEnvs binds each option to the corresponding environment variable name. The environment variable is obtained by
// converting each option name to upper snake case. Each option name is assumed to be in mixed case.
func bindEnvs(v *viper.Viper) error {
var err error
for _, option := range options {
if err = v.BindEnv(option, mixedCaseToUpperSnakeCase(option)); err != nil {
return err
}
}
return nil
}
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
// mixedCaseToUpperSnakeCase converts mixed case string into upper snake case string
func mixedCaseToUpperSnakeCase(s string) string {
snake := matchFirstCap.ReplaceAllString(s, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
snake = strings.ReplaceAll(strings.ToUpper(snake), ".", "_")
return snake
}
Editor is loading...