github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/config/util.go (about)

     1  // Copyright (c) 2018-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package config
     6  
     7  import (
     8  	"bufio"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  
    18  	"github.com/choria-io/go-choria/confkey"
    19  	iu "github.com/choria-io/go-choria/internal/util"
    20  )
    21  
    22  // ProjectConfigurationFiles returns any configuration file in the specified directory and their parents directories.
    23  func ProjectConfigurationFiles(path string) ([]string, error) {
    24  	var (
    25  		res []string
    26  		err error
    27  	)
    28  
    29  	if !filepath.IsAbs(path) {
    30  		path, err = filepath.Abs(path)
    31  		if err != nil {
    32  			return nil, err
    33  		}
    34  	}
    35  
    36  	var parent = filepath.Dir(path)
    37  	if parent != path {
    38  		res, err = ProjectConfigurationFiles(parent)
    39  		if err != nil {
    40  			return nil, err
    41  		}
    42  	}
    43  
    44  	config := filepath.Join(path, "choria.conf")
    45  	if iu.FileExist(config) {
    46  		res = append(res, config)
    47  	}
    48  
    49  	return res, nil
    50  }
    51  
    52  // DNSFQDN attempts to find the FQDN using DNS resolution
    53  func DNSFQDN() (string, error) {
    54  	hostname, err := os.Hostname()
    55  	if err != nil {
    56  		return "", err
    57  	}
    58  
    59  	addrs, err := net.LookupIP(hostname)
    60  	if err != nil {
    61  		return "", err
    62  	}
    63  
    64  	for _, addr := range addrs {
    65  		if ipv4 := addr.To4(); ipv4 != nil {
    66  			ip, err := ipv4.MarshalText()
    67  			if err != nil {
    68  				return "", err
    69  			}
    70  
    71  			hosts, err := net.LookupAddr(string(ip))
    72  			if err != nil || len(hosts) == 0 {
    73  				return "", err
    74  			}
    75  
    76  			fqdn := hosts[0]
    77  
    78  			// return fqdn without trailing dot
    79  			return strings.TrimSuffix(fqdn, "."), nil
    80  		}
    81  	}
    82  
    83  	return "", fmt.Errorf("could not resolve FQDN using DNS")
    84  }
    85  
    86  // can be used to extract the parsed settings
    87  func parseDotConfFile(plugin string, conf *Config, target any) error {
    88  	cfgPath := filepath.Join(conf.dotdDir(), fmt.Sprintf("%s.cfg", plugin))
    89  	if iu.FileExist(cfgPath) {
    90  		err := parseConfig(cfgPath, target, fmt.Sprintf("plugin.%s", plugin), conf.rawOpts)
    91  		if err != nil {
    92  			return err
    93  		}
    94  
    95  		conf.ParsedFiles = append(conf.ParsedFiles, cfgPath)
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  // parseAllDotCfg parses a file like /etc/..../plugin.d/package.cfg as if its full of
   102  // plugin.package.x = y lines and fill in a structure with the results if that structure
   103  // declares its options using the same tag structure as Config.
   104  //
   105  // If the supplied target structure is nil then the only side effect will be that the
   106  // supplied conf will be updated with the raw options so that HasOption() and Option()
   107  func (c *Config) parseAllDotCfg() error {
   108  	dir := c.dotdDir()
   109  	if dir == "" {
   110  		return nil
   111  	}
   112  
   113  	if !iu.FileIsDir(dir) {
   114  		return nil
   115  	}
   116  
   117  	files, err := os.ReadDir(dir)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	for _, file := range files {
   123  		ext := filepath.Ext(file.Name())
   124  		if ext == ".cfg" || ext == ".conf" {
   125  			base := path.Base(file.Name())
   126  			var target any
   127  
   128  			if base == "choria.cfg" {
   129  				target = c.Choria
   130  			}
   131  
   132  			plugin := strings.TrimSuffix(base, filepath.Ext(base))
   133  			err = parseDotConfFile(plugin, c, target)
   134  			if err != nil {
   135  				return err
   136  			}
   137  		}
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  // parse a config file and fill in the given config structure based on its tags
   144  func parseConfig(path string, config any, prefix string, found map[string]string) error {
   145  	file, err := os.Open(path)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	defer file.Close()
   150  
   151  	c, ok := config.(*Config)
   152  	if ok {
   153  		c.ParsedFiles = append(c.ParsedFiles, path)
   154  	}
   155  
   156  	parseConfigContents(file, config, prefix, found)
   157  
   158  	return nil
   159  }
   160  
   161  func parseConfigContents(content io.Reader, config any, prefix string, found map[string]string) {
   162  	scanner := bufio.NewScanner(content)
   163  	itemr := regexp.MustCompile(`(.+?)\s*=\s*(.+)`)
   164  	skipr := regexp.MustCompile(`^#|^$`)
   165  
   166  	for scanner.Scan() {
   167  		line := strings.TrimSpace(scanner.Text())
   168  
   169  		if !skipr.MatchString(line) {
   170  			if itemr.MatchString(line) {
   171  				matches := itemr.FindStringSubmatch(line)
   172  				var key string
   173  
   174  				if prefix == "" {
   175  					key = matches[1]
   176  				} else {
   177  					key = prefix + "." + matches[1]
   178  				}
   179  
   180  				if config != nil {
   181  					// errors here are normal since items for Choria and Config are in the same file
   182  					confkey.SetStructFieldWithKey(config, key, matches[2])
   183  				}
   184  
   185  				found[key] = matches[2]
   186  			}
   187  		}
   188  	}
   189  }