github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/discovery/flatfile/flatfile.go (about) 1 // Copyright (c) 2021-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package flatfile 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "encoding/json" 12 "fmt" 13 "io" 14 "os" 15 "regexp" 16 "strings" 17 "time" 18 19 "github.com/choria-io/go-choria/inter" 20 "github.com/tidwall/gjson" 21 22 "github.com/choria-io/go-choria/filter/identity" 23 "github.com/choria-io/go-choria/providers/agent/mcorpc/replyfmt" 24 25 "github.com/ghodss/yaml" 26 "github.com/sirupsen/logrus" 27 ) 28 29 const validIdentity = `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` 30 31 var validIdentityRe = regexp.MustCompile(validIdentity) 32 33 type FlatFile struct { 34 fw inter.Framework 35 timeout time.Duration 36 log *logrus.Entry 37 } 38 39 func New(fw inter.Framework) *FlatFile { 40 return &FlatFile{ 41 fw: fw, 42 timeout: time.Second * time.Duration(fw.Configuration().DiscoveryTimeout), 43 log: fw.Logger("flatfile_discovery"), 44 } 45 } 46 47 func (f *FlatFile) Discover(_ context.Context, opts ...DiscoverOption) (n []string, err error) { 48 dopts := &dOpts{do: make(map[string]string)} 49 50 for _, opt := range opts { 51 opt(dopts) 52 } 53 54 if dopts.filter != nil { 55 if len(dopts.filter.Agent) > 0 || len(dopts.filter.Compound) > 0 || len(dopts.filter.Class) > 0 || len(dopts.filter.Fact) > 0 { 56 return nil, fmt.Errorf("only identity filters are supported") 57 } 58 } 59 60 file, ok := dopts.do["file"] 61 if ok { 62 dopts.reader = nil 63 dopts.source = file 64 } 65 66 format, ok := dopts.do["format"] 67 if ok { 68 switch format { 69 case "json": 70 dopts.format = JSONFormat 71 case "yaml", "yml": 72 dopts.format = YAMLFormat 73 case "choriarpc", "results", "rpc", "response": 74 dopts.format = ChoriaResponsesFormat 75 default: 76 dopts.format = TextFormat 77 } 78 } 79 80 if dopts.source == "" && dopts.reader == nil { 81 return nil, fmt.Errorf("source file not specified") 82 } 83 84 if dopts.reader == nil { 85 sf, err := os.Open(dopts.source) 86 if err != nil { 87 return nil, err 88 } 89 defer sf.Close() 90 91 dopts.reader = sf 92 } 93 94 var nodes []string 95 96 switch dopts.format { 97 case TextFormat, unknownFormat: 98 nodes, err = f.textDiscover(dopts.reader) 99 100 case JSONFormat: 101 nodes, err = f.jsonDiscover(dopts.reader, dopts.do) 102 103 case YAMLFormat: 104 nodes, err = f.yamlDiscover(dopts.reader, dopts.do) 105 106 case ChoriaResponsesFormat: 107 nodes, err = f.choriaDiscover(dopts.reader) 108 109 default: 110 return nil, fmt.Errorf("unknown file format") 111 } 112 113 if err != nil { 114 return nil, err 115 } 116 117 err = f.validateNodes(nodes) 118 if err != nil { 119 return nil, err 120 } 121 122 if dopts.filter != nil && len(dopts.filter.Identity) > 0 { 123 matched := []string{} 124 for _, idf := range dopts.filter.Identity { 125 matched = append(matched, identity.FilterNodes(nodes, idf)...) 126 } 127 return matched, nil 128 } 129 130 return nodes, nil 131 } 132 133 func (f *FlatFile) validateNodes(nodes []string) error { 134 for _, n := range nodes { 135 if !validIdentityRe.MatchString(n) { 136 return fmt.Errorf("invalid identity string %q", n) 137 } 138 } 139 140 return nil 141 } 142 143 func (f *FlatFile) choriaDiscover(file io.Reader) ([]string, error) { 144 raw, err := io.ReadAll(file) 145 if err != nil { 146 return nil, err 147 } 148 149 input := bytes.TrimSpace(raw) 150 if len(input) < 2 { 151 return nil, fmt.Errorf("did not detect valid JSON data") 152 } 153 154 if input[0] != '{' && input[len(input)-1] != '}' { 155 return nil, fmt.Errorf("did not detect valid JSON data") 156 } 157 158 data := replyfmt.RPCResults{} 159 err = json.Unmarshal(input, &data) 160 if err != nil { 161 return nil, err 162 } 163 164 found := []string{} 165 for _, reply := range data.Replies { 166 found = append(found, reply.Sender) 167 } 168 169 return found, nil 170 } 171 172 func (f *FlatFile) yamlDiscover(file io.Reader, do map[string]string) ([]string, error) { 173 data, err := io.ReadAll(file) 174 if err != nil { 175 return nil, err 176 } 177 178 jdata, err := yaml.YAMLToJSON(data) 179 if err != nil { 180 return nil, err 181 } 182 183 return f.jsonDiscover(bytes.NewReader(jdata), do) 184 } 185 186 func (f *FlatFile) jsonDiscover(file io.Reader, do map[string]string) ([]string, error) { 187 data, err := io.ReadAll(file) 188 if err != nil { 189 return nil, err 190 } 191 192 nodes := []string{} 193 filter, ok := do["filter"] 194 if ok { 195 if filter == "" { 196 return nil, fmt.Errorf("empty filter string found in discovery options") 197 } 198 199 res := gjson.GetBytes(data, filter) 200 if res.IsArray() { 201 res.ForEach(func(_ gjson.Result, v gjson.Result) bool { 202 if v.Exists() && v.Type == gjson.String { 203 nodes = append(nodes, v.String()) 204 } 205 206 return true 207 }) 208 return nodes, nil 209 } else { 210 return nodes, fmt.Errorf("query %q did not result in a array of nodes", filter) 211 } 212 } 213 214 err = json.Unmarshal(data, &nodes) 215 if err != nil { 216 return nil, err 217 } 218 219 return nodes, nil 220 } 221 222 func (f *FlatFile) textDiscover(file io.Reader) ([]string, error) { 223 var found []string 224 scanner := bufio.NewScanner(file) 225 for scanner.Scan() { 226 found = append(found, strings.TrimSpace(scanner.Text())) 227 } 228 229 err := scanner.Err() 230 if err != nil { 231 return nil, err 232 } 233 234 return found, nil 235 }