github.com/etecs-ru/gnomock@v0.13.2/preset/splunk/init.go (about)

     1  package splunk
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"strconv"
    15  	"time"
    16  
    17  	"github.com/etecs-ru/gnomock"
    18  )
    19  
    20  var errConflict = fmt.Errorf("409: conflict")
    21  
    22  // Event is a type used during Splunk initialization. Pass events to WithValues
    23  // to ingest them into the container before the control over it is passed to
    24  // the caller
    25  type Event struct {
    26  	// Event is the actual log entry. Can be any format
    27  	Event string `json:"event"`
    28  
    29  	// Index is the name of index to ingest the log into. If the index does not
    30  	// exist, it will be created
    31  	Index string `json:"index"`
    32  
    33  	// Source will be used as "source" value of this event in Splunk
    34  	Source string `json:"source"`
    35  
    36  	// SourceType will be used as "sourcetype" value of this event in Splunk
    37  	SourceType string `json:"sourcetype"`
    38  
    39  	// Time represents event timestamp in seconds, milliseconds or nanoseconds
    40  	// (and maybe even in microseconds, whatever splunk recognizes)
    41  	Time int64 `json:"time"`
    42  }
    43  
    44  func (p *P) initf() gnomock.InitFunc {
    45  	return func(ctx context.Context, c *gnomock.Container) (err error) {
    46  		if p.ValuesFile != "" {
    47  			f, err := os.Open(p.ValuesFile)
    48  			if err != nil {
    49  				return fmt.Errorf("can't open values file '%s': %w", p.ValuesFile, err)
    50  			}
    51  
    52  			defer func() {
    53  				closeErr := f.Close()
    54  				if err == nil && closeErr != nil {
    55  					err = closeErr
    56  				}
    57  			}()
    58  
    59  			events := make([]Event, 0)
    60  			decoder := json.NewDecoder(f)
    61  
    62  			for {
    63  				var e Event
    64  
    65  				err = decoder.Decode(&e)
    66  				if errors.Is(err, io.EOF) {
    67  					break
    68  				}
    69  
    70  				if err != nil {
    71  					return fmt.Errorf("can't read initial event: %w", err)
    72  				}
    73  
    74  				events = append(events, e)
    75  			}
    76  
    77  			p.Values = append(events, p.Values...)
    78  		}
    79  
    80  		err = Ingest(ctx, c, p.AdminPassword, p.Values...)
    81  		if err != nil {
    82  			return fmt.Errorf("can't ingest events: %w", err)
    83  		}
    84  
    85  		return nil
    86  	}
    87  }
    88  
    89  // Ingest adds the provided events to splunk container. Use the same password
    90  // you provided in WithPassword. Send as many events as you like, this function
    91  // only returns when all the events were indexed, or when the context is timed
    92  // out
    93  func Ingest(ctx context.Context, c *gnomock.Container, password string, events ...Event) error {
    94  	postForm := requestWithPassword(http.MethodPost, password, false)
    95  	apiAddr := c.Address(APIPort)
    96  
    97  	token, err := issueToken(postForm, apiAddr)
    98  	if err != nil {
    99  		return fmt.Errorf("can't issue new HEC token: %w", err)
   100  	}
   101  
   102  	ensureIndex := indexRegistry(postForm, apiAddr)
   103  	ingestEvent := eventForwarder(
   104  		requestWithPassword(http.MethodPost, token, true),
   105  		c.Address(CollectorPort),
   106  	)
   107  
   108  	initialCount, err := countEvents(postForm, apiAddr)
   109  	if err != nil {
   110  		return fmt.Errorf("can't get initial event count: %w", err)
   111  	}
   112  
   113  	for _, e := range events {
   114  		select {
   115  		case <-ctx.Done():
   116  			return context.Canceled
   117  		default:
   118  			err := ensureIndex(e.Index)
   119  			if err != nil {
   120  				return err
   121  			}
   122  
   123  			err = ingestEvent(e)
   124  			if err != nil {
   125  				return err
   126  			}
   127  		}
   128  	}
   129  
   130  	var (
   131  		lastErr       error
   132  		lastCount     int
   133  		expectedCount int = initialCount + len(events)
   134  	)
   135  
   136  	for {
   137  		select {
   138  		case <-ctx.Done():
   139  			return fmt.Errorf("event count didn't match: want %d, got %d; last error: %v: %w",
   140  				len(events), lastCount, lastErr, context.Canceled)
   141  		default:
   142  			lastCount, lastErr = countEvents(postForm, apiAddr)
   143  			if lastErr == nil && lastCount == expectedCount {
   144  				return nil
   145  			}
   146  
   147  			time.Sleep(time.Millisecond * 250)
   148  		}
   149  	}
   150  }
   151  
   152  func issueToken(post postFunc, addr string) (string, error) {
   153  	newTokenURL := fmt.Sprintf("https://%s/services/data/inputs/http?output_mode=json", addr)
   154  	tokenName := fmt.Sprintf("gnomock-%d", time.Now().UnixNano())
   155  
   156  	data := url.Values{}
   157  	data.Set("name", tokenName)
   158  	buf := bytes.NewBufferString(data.Encode())
   159  
   160  	bs, err := post(newTokenURL, buf)
   161  	if err != nil {
   162  		return "", fmt.Errorf("can't create new HEC token: %w", err)
   163  	}
   164  
   165  	r := splunkTokenResponse{}
   166  
   167  	err = json.Unmarshal(bs, &r)
   168  	if err != nil {
   169  		return "", fmt.Errorf("can't unmarshal HEC token: %w", err)
   170  	}
   171  
   172  	return r.Entry[0].Content.Token, nil
   173  }
   174  
   175  func indexRegistry(post postFunc, addr string) func(string) error {
   176  	indexes := map[string]bool{"main": true}
   177  	uri := fmt.Sprintf("https://%s/services/data/indexes?output_mode=json", addr)
   178  
   179  	return func(indexName string) error {
   180  		if _, ok := indexes[indexName]; !ok {
   181  			_, err := post(uri, bytes.NewBufferString("name="+indexName))
   182  			if err != nil && !errors.Is(err, errConflict) {
   183  				return fmt.Errorf("can't create index: %w", err)
   184  			}
   185  
   186  			indexes[indexName] = true
   187  		}
   188  
   189  		return nil
   190  	}
   191  }
   192  
   193  func eventForwarder(post postFunc, addr string) func(Event) error {
   194  	return func(e Event) error {
   195  		uri := fmt.Sprintf("https://%s/services/collector?output_mode=json", addr)
   196  
   197  		eventBytes, err := json.Marshal(e)
   198  		if err != nil {
   199  			return fmt.Errorf("can't marshal event to json: %w", err)
   200  		}
   201  
   202  		_, err = post(uri, bytes.NewBuffer(eventBytes))
   203  		if err != nil {
   204  			return err
   205  		}
   206  
   207  		return nil
   208  	}
   209  }
   210  
   211  func countEvents(post postFunc, addr string) (int, error) {
   212  	uri := fmt.Sprintf("https://%s/services/search/jobs/export", addr)
   213  	data := url.Values{}
   214  	data.Add("search", "search index=* | stats count")
   215  	data.Add("output_mode", "json")
   216  
   217  	bs, err := post(uri, bytes.NewBufferString(data.Encode()))
   218  	if err != nil {
   219  		return 0, err
   220  	}
   221  
   222  	var response splunkSearchcResponse
   223  
   224  	err = json.Unmarshal(bs, &response)
   225  	if err != nil {
   226  		return 0, err
   227  	}
   228  
   229  	countStr, ok := response.Result["count"]
   230  	if !ok {
   231  		return 0, err
   232  	}
   233  
   234  	count, err := strconv.Atoi(fmt.Sprintf("%s", countStr))
   235  	if err != nil {
   236  		return 0, err
   237  	}
   238  
   239  	return count, nil
   240  }
   241  
   242  type postFunc func(string, *bytes.Buffer) ([]byte, error)
   243  
   244  func requestWithPassword(method, password string, isJSON bool) postFunc {
   245  	client := insecureClient()
   246  
   247  	return func(uri string, buf *bytes.Buffer) (bs []byte, err error) {
   248  		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   249  		defer cancel()
   250  
   251  		req, err := http.NewRequestWithContext(ctx, method, uri, buf)
   252  		if err != nil {
   253  			return nil, fmt.Errorf("can't create request: %w", err)
   254  		}
   255  
   256  		req.SetBasicAuth("admin", password)
   257  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   258  
   259  		if isJSON {
   260  			req.Header.Set("Content-Type", "application/json")
   261  		}
   262  
   263  		resp, err := client.Do(req)
   264  		if err != nil {
   265  			return nil, fmt.Errorf("request failed: %w", err)
   266  		}
   267  
   268  		defer func() {
   269  			closeErr := resp.Body.Close()
   270  			if err == nil && closeErr != nil {
   271  				err = fmt.Errorf("can't close response body: %w", closeErr)
   272  			}
   273  		}()
   274  
   275  		bs, err = ioutil.ReadAll(resp.Body)
   276  		if err != nil {
   277  			return nil, fmt.Errorf("can't read response body: %w", err)
   278  		}
   279  
   280  		if resp.StatusCode == http.StatusConflict {
   281  			return nil, errConflict
   282  		}
   283  
   284  		if resp.StatusCode >= http.StatusBadRequest {
   285  			return nil, errors.New(resp.Status + ": " + string(bs))
   286  		}
   287  
   288  		return bs, nil
   289  	}
   290  }
   291  
   292  type splunkTokenResponse struct {
   293  	Entry []struct {
   294  		Content struct {
   295  			Token string `json:"token"`
   296  		} `json:"content"`
   297  	} `json:"entry"`
   298  }
   299  
   300  type splunkSearchcResponse struct {
   301  	Result map[string]interface{} `json:"result"`
   302  }