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 }