Files
utilities/security/openbao-monitor/source/config/config.go
michel-thebeau-WR bc138e563c openbao-monitor: align the log levels with helm chart
The helm chart uses numeric values representing the following log
levels: DEBUG, INFO, WARNING, ERROR and FATAL

Update the validation to accept numeric levels, and interpet 5 (FATAL)
as ERROR.

Test Plan:
PASS  test valid/invalid loglevels, numeric bounds

Change-Id: Ifbebe30e487f30ba11ada398056252e80a07ba94
Signed-off-by: Michel Thebeau <Michel.Thebeau@windriver.com>
2025-08-15 12:51:12 +00:00

321 lines
9.8 KiB
Go

//
// Copyright (c) 2025 Wind River Systems, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
package baoConfig
import (
"fmt"
"io"
"log/slog"
"strconv"
"strings"
"time"
"github.com/go-yaml/yaml"
clientapi "github.com/openbao/openbao/api/v2"
)
type ServerAddress struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
}
type Token struct {
Duration int `yaml:"duration"`
Key string `yaml:"key"`
}
type KeyShards struct {
Key string `yaml:"key"`
KeyBase64 string `yaml:"key_base64"`
}
type MonitorConfig struct {
// A map value listing all DNS names
// Key: Domain name
// Value: ServerAddress. consisting of host address and port number
ServerAddresses map[string]ServerAddress `yaml:"ServerAddresses"`
// A map value listing all authentication tokens
// Key: release id
// Value: Token. consisting of lease duration and the token key
Tokens map[string]Token `yaml:"Tokens"`
// A map value listing all key shards for unseal
// Key: shard name
// Value: The shard key and the base64 encoded version of that key
UnsealKeyShards map[string]KeyShards `yaml:"UnsealKeyShards"`
// A string of path to the PEM-encoded CA cert file to use to verify
// The server's SSL certificate
// Leave this empty if using the default CA cert file location
CACert string `yaml:"CACert"`
// ClientCert is the path to the certificate for Vault communication
ClientCert string `yaml:"ClientCert"`
// ClientKey is the path to the private key for Vault communication
ClientKey string `yaml:"ClientKey"`
// The path of the log file
LogPath string `yaml:"logPath"`
// The default log level
// Available log levels: DEBUG, INFO, WARN and ERROR
// Accepts numeric values matching helm chart log levels (4-5 are interpreted as ERROR)
LogLevel string `yaml:"logLevel"`
// The time in seconds waited between each unseal check in the run command.
// If this is unset or set to 0, the command option can be used to supply the time.
// If neither is supplied, then default time of 5 seconds will be used.
WaitInterval int `yaml:"WaitInterval"`
// Time, in seconds, the client will wait for each request before
// returning timeout exceeded error.
// Set this value in negative to use the default value of 60 seconds.
Timeout int `yaml:"Timeout"`
// Namespace used for the k8s application.
Namespace string `yaml:"Namespace"`
// Default port for all addresses.
// If the port number was not specified in the config file, it will use this port number.
// This port number will also be used for all generated addresses from Kubernetes pods
// Default value is always 8200
DefaultPort int `yaml:"DefaultPort"`
// Prefix string used to find all server pods
PodPrefix string `yaml:"PodPrefix"`
// Suffix string for all generated pod addresses
// Default is "pod.cluster.local"
PodAddressSuffix string `yaml:"PodAddressSuffix"`
// Prefix string used to find root token and unseal key shards
// Default is "cluster-key"
SecretPrefix string `yaml:"SecretPrefix"`
}
func (configInstance *MonitorConfig) ReadYAMLMonitorConfig(in io.Reader) error {
data, err := io.ReadAll(in)
if err != nil {
return fmt.Errorf(
"unable to read Host DNS config data from input. Error message: %v", err)
}
err = yaml.Unmarshal(data, configInstance)
if err != nil {
return fmt.Errorf(
"unable to unmarshal Host DNS config YAML data. Error message: %v", err)
}
// Use default port value of 8200, if no default port was specified.
if configInstance.DefaultPort == 0 {
configInstance.DefaultPort = 8200
}
// Fill in empty ports
for dnsname, addr := range configInstance.ServerAddresses {
if addr.Port == 0 {
addr.Port = configInstance.DefaultPort
configInstance.ServerAddresses[dnsname] = addr
}
}
// Validate YAML input for ServerAddresses
err = configInstance.validateDNS()
if err != nil {
return err
}
// Validate YAML input for Tokens
err = configInstance.validateTokens()
if err != nil {
return err
}
// Validate YAML input for unseal key shards
err = configInstance.validateKeyShards()
if err != nil {
return err
}
// Validate YAML input for CACert
err = configInstance.validateCACert()
if err != nil {
return err
}
// Validate YAML input for log configs
err = configInstance.validateLogConfig()
if err != nil {
return err
}
return nil
}
func (configInstance MonitorConfig) WriteYAMLMonitorConfig(out io.Writer) error {
data, err := yaml.Marshal(configInstance)
if err != nil {
return fmt.Errorf(
"unable to marshal Host DNS config data to YAML. Error message: %v", err)
}
_, err = out.Write(data)
if err != nil {
return fmt.Errorf(
"unable to write marshaled Host DNS config YAML data. Error message: %v", err)
}
return nil
}
// Create a new config based on the monitor config
func (configInstance MonitorConfig) NewConfig(dnshost string) (*clientapi.Config, error) {
slog.Debug(fmt.Sprintf("Setting up api access config for host %v", dnshost))
defConfig := clientapi.DefaultConfig()
// Check if DefaultConfig has issues
if defConfig.Error != nil {
return defConfig, fmt.Errorf("issue found in default config: %v", defConfig.Error)
}
slog.Debug("No issues found in retrieving default config.")
// Check if there is a domain name listed under ServerAddresses
dnsAddr, ok := configInstance.ServerAddresses[dnshost]
if !ok {
return defConfig, fmt.Errorf("unable to find %v under the list of available DNS names", dnshost)
}
// Set the DNS address as the configured address for the server
defConfig.Address = strings.Join([]string{"https://", dnsAddr.Host, ":", strconv.Itoa(dnsAddr.Port)}, "")
slog.Debug(fmt.Sprintf("Server address set to %v", defConfig.Address))
// Apply CACert entry to the config
var newTLSconfig clientapi.TLSConfig
slog.Debug("Applying the following cert configs:")
slog.Debug(fmt.Sprintf("CACert: %v", configInstance.CACert))
slog.Debug(fmt.Sprintf("ClientCert: %v", configInstance.ClientCert))
slog.Debug(fmt.Sprintf("ClientKey: %v", configInstance.ClientKey))
newTLSconfig.CACert = configInstance.CACert
newTLSconfig.ClientCert = configInstance.ClientCert
newTLSconfig.ClientKey = configInstance.ClientKey
// This does nothing if newTLSconfig is empty
err := defConfig.ConfigureTLS(&newTLSconfig)
if err != nil {
return defConfig, fmt.Errorf("error with configuring TLS: %v", err)
}
slog.Debug("Configuring TLS successful")
// Set the timeout value. Do not set the value if it is negative.
if configInstance.Timeout >= 0 {
defConfig.Timeout = time.Duration(configInstance.Timeout) * time.Second
}
slog.Debug("API access config setup complete.")
// Config creation complete.
return defConfig, nil
}
func (configInstance MonitorConfig) SetupClient(dnshost string) (*clientapi.Client, error) {
slog.Debug(fmt.Sprintf("Setting up client for host %v", dnshost))
newConfig, err := configInstance.NewConfig(dnshost)
if err != nil {
return nil, fmt.Errorf("error in creating new config: %v", err)
}
slog.Debug("Creating client for API access...")
newClient, err := clientapi.NewClient(newConfig)
if err != nil {
return nil, fmt.Errorf("error in creating new client: %v", err)
}
slog.Debug("Client setup complete.")
return newClient, nil
}
// Parse the new keys from the init responce into the monitor config
func (configInstance *MonitorConfig) ParseInitResponse(dnshost string, responce *clientapi.InitResponse) error {
slog.Debug("Parsing response from /sys/init to monitor configs")
keyShardheader := strings.Join([]string{"key", "shard", dnshost}, "-")
slog.Debug("Parsing the root token...")
// Parse in the root token
if _, ok := configInstance.Tokens["root_token"]; ok {
return fmt.Errorf("an entry of the root token was already found")
}
if configInstance.Tokens == nil {
configInstance.Tokens = make(map[string]Token)
}
configInstance.Tokens["root_token"] = Token{
Duration: 0,
Key: responce.RootToken,
}
slog.Debug("Parsing the unseal key shards...")
// Parse in the key shards for unseal
for i := range len(responce.Keys) {
keyShardName := strings.Join([]string{keyShardheader, strconv.Itoa(i)}, "-")
if _, ok := configInstance.UnsealKeyShards[keyShardName]; ok {
return fmt.Errorf("an entry of %v was already found under UnsealKeyShards", keyShardName)
}
if configInstance.UnsealKeyShards == nil {
configInstance.UnsealKeyShards = make(map[string]KeyShards)
}
configInstance.UnsealKeyShards[keyShardName] = KeyShards{
Key: responce.Keys[i],
KeyBase64: responce.KeysB64[i],
}
}
slog.Debug("Parsing the recovery key shards...")
// Parse in the recovery key shards
for i := range len(responce.RecoveryKeys) {
keyShardName := strings.Join([]string{keyShardheader, "recovery", strconv.Itoa(i)}, "-")
if _, ok := configInstance.UnsealKeyShards[keyShardName]; ok {
return fmt.Errorf("an entry of %v was already found under UnsealKeyShards", keyShardName)
}
if configInstance.UnsealKeyShards == nil {
configInstance.UnsealKeyShards = make(map[string]KeyShards)
}
configInstance.UnsealKeyShards[keyShardName] = KeyShards{
Key: responce.RecoveryKeys[i],
KeyBase64: responce.RecoveryKeysB64[i],
}
}
slog.Debug("Parsing init response complete")
return nil
}
// Interpret numeric or text log level
// Always returns a log level in string format
func (configInstance *MonitorConfig) InterpretLogLevel() string {
// Check if the log level is a number
if converted, err := strconv.Atoi(configInstance.LogLevel); err == nil {
if level, exists := availableLogLevels[converted]; exists {
return level
}
// error, but this code should not be reached if validateLogConfig works
fmt.Errorf("the numeric LogLevel %v is not a valid log level", configInstance.LogLevel)
return "INFO" // Default to INFO if the numeric level is invalid
}
// Default to INFO if no log level was set
if configInstance.LogLevel == "" {
return "INFO"
}
// validateLogConfig already validated the LogLevel
return configInstance.LogLevel
}