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  }