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

     1  // Package elastic provides a Gnomock Preset for Elasticsearch.
     2  package elastic
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"time"
    14  
    15  	"github.com/elastic/go-elasticsearch/v7"
    16  	"github.com/etecs-ru/gnomock"
    17  	"github.com/etecs-ru/gnomock/internal/registry"
    18  )
    19  
    20  const (
    21  	defaultVersion = "7.9.3"
    22  	defaultPort    = 9200
    23  )
    24  
    25  func init() {
    26  	registry.Register("elastic", func() gnomock.Preset { return &P{} })
    27  }
    28  
    29  // Preset creates a new Gmomock Elasticsearch preset. This preset includes an
    30  // Elasticsearch specific healthcheck function and default Elasticsearch image
    31  // and port.
    32  //
    33  // By default, version 7.9.3 is used.
    34  func Preset(opts ...Option) gnomock.Preset {
    35  	p := &P{}
    36  
    37  	for _, opt := range opts {
    38  		opt(p)
    39  	}
    40  
    41  	return p
    42  }
    43  
    44  // P is a Gnomock Preset implementation of Elasticsearch.
    45  type P struct {
    46  	Version string   `json:"version"`
    47  	Inputs  []string `json:"input_files"`
    48  }
    49  
    50  // Image returns an image that should be pulled to create this container.
    51  func (p *P) Image() string {
    52  	return fmt.Sprintf("docker.io/library/elasticsearch:%s", p.Version)
    53  }
    54  
    55  // Ports returns ports that should be used to access this container.
    56  func (p *P) Ports() gnomock.NamedPorts {
    57  	return gnomock.DefaultTCP(defaultPort)
    58  }
    59  
    60  // Options returns a list of options to configure this container.
    61  func (p *P) Options() []gnomock.Option {
    62  	p.setDefaults()
    63  
    64  	opts := []gnomock.Option{
    65  		gnomock.WithEnv("discovery.type=single-node"),
    66  		gnomock.WithEnv("ES_JAVA_OPTS=-Xms256m -Xmx256m"),
    67  		gnomock.WithHealthCheck(p.healthcheck),
    68  	}
    69  
    70  	if len(p.Inputs) > 0 {
    71  		opts = append(opts, gnomock.WithInit(p.initf))
    72  	}
    73  
    74  	return opts
    75  }
    76  
    77  func (p *P) healthcheck(ctx context.Context, c *gnomock.Container) (err error) {
    78  	defaultAddr := fmt.Sprintf("http://%s", c.DefaultAddress())
    79  
    80  	cfg := elasticsearch.Config{
    81  		Addresses:    []string{defaultAddr},
    82  		DisableRetry: true,
    83  	}
    84  
    85  	client, err := elasticsearch.NewClient(cfg)
    86  	if err != nil {
    87  		return fmt.Errorf("can't create elasticsearch client: %w", err)
    88  	}
    89  
    90  	res, err := client.Info()
    91  	if err != nil {
    92  		return fmt.Errorf("can't get cluster info: %w", err)
    93  	}
    94  
    95  	defer func() {
    96  		closeErr := res.Body.Close()
    97  		if closeErr != nil && err == nil {
    98  			err = closeErr
    99  		}
   100  	}()
   101  
   102  	if res.IsError() {
   103  		return fmt.Errorf("cluster info failed: %s", res.String())
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  func (p *P) initf(ctx context.Context, c *gnomock.Container) (err error) {
   110  	defaultAddr := fmt.Sprintf("http://%s", c.DefaultAddress())
   111  
   112  	cfg := elasticsearch.Config{
   113  		Addresses:    []string{defaultAddr},
   114  		DisableRetry: true,
   115  	}
   116  
   117  	client, err := elasticsearch.NewClient(cfg)
   118  	if err != nil {
   119  		return fmt.Errorf("can't create elasticsearch client: %w", err)
   120  	}
   121  
   122  	docCount := 0
   123  
   124  	for _, file := range p.Inputs {
   125  		select {
   126  		case <-ctx.Done():
   127  			return context.Canceled
   128  		default:
   129  			n, err := p.ingestFile(file, client)
   130  			if err != nil {
   131  				return fmt.Errorf("can't ingest file '%s': %w", file, err)
   132  			}
   133  
   134  			docCount += n
   135  		}
   136  	}
   137  
   138  	tick := time.NewTicker(time.Millisecond * 250)
   139  	defer tick.Stop()
   140  
   141  	for {
   142  		select {
   143  		case <-ctx.Done():
   144  			return context.Canceled
   145  		case <-tick.C:
   146  			total, err := p.totalDocCount(client)
   147  			if err != nil {
   148  				return fmt.Errorf("can't count docs: %w", err)
   149  			}
   150  
   151  			if total == docCount {
   152  				return nil
   153  			}
   154  		}
   155  	}
   156  }
   157  
   158  func (p *P) totalDocCount(client *elasticsearch.Client) (n int, err error) {
   159  	res, err := client.Indices.Stats(client.Indices.Stats.WithFilterPath("_all.total.docs.count"))
   160  	if err != nil {
   161  		return 0, fmt.Errorf("failed to get index status: %w", err)
   162  	}
   163  
   164  	defer func() {
   165  		closeErr := res.Body.Close()
   166  		if err == nil && closeErr != nil {
   167  			err = closeErr
   168  		}
   169  	}()
   170  
   171  	if res.IsError() {
   172  		return 0, fmt.Errorf("invalid response for index status: %s", res.String())
   173  	}
   174  
   175  	var out struct {
   176  		All struct {
   177  			Total struct {
   178  				Docs struct {
   179  					Count int `json:"count"`
   180  				} `json:"docs"`
   181  			} `json:"total"`
   182  		} `json:"_all"`
   183  	}
   184  
   185  	if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
   186  		return 0, fmt.Errorf("invalid output for index stats: %w", err)
   187  	}
   188  
   189  	return out.All.Total.Docs.Count, nil
   190  }
   191  
   192  func (p *P) ingestFile(fName string, client *elasticsearch.Client) (docCount int, err error) {
   193  	f, err := os.Open(fName) // nolint:gosec
   194  	if err != nil {
   195  		return 0, fmt.Errorf("can't open file '%s': %w", fName, err)
   196  	}
   197  
   198  	defer func() {
   199  		closeErr := f.Close()
   200  		if closeErr != nil && err == nil {
   201  			err = closeErr
   202  		}
   203  	}()
   204  
   205  	for decoder := json.NewDecoder(f); ; {
   206  		var v interface{}
   207  
   208  		if err := decoder.Decode(&v); err != nil {
   209  			if errors.Is(err, io.EOF) {
   210  				break
   211  			}
   212  
   213  			return docCount, fmt.Errorf("can't decode json data: %w", err)
   214  		}
   215  
   216  		bs, err := json.Marshal(v)
   217  		if err != nil {
   218  			return docCount, err
   219  		}
   220  
   221  		if err := p.ingestData(path.Base(fName), bs, client); err != nil {
   222  			return 0, fmt.Errorf("failed to ingest data: %w", err)
   223  		}
   224  
   225  		docCount++
   226  	}
   227  
   228  	return docCount, nil
   229  }
   230  
   231  func (p *P) ingestData(index string, bs []byte, client *elasticsearch.Client) (err error) {
   232  	res, err := client.Index(index, bytes.NewBuffer(bs))
   233  	if err != nil {
   234  		return fmt.Errorf("failed to index file '%s': %w", index, err)
   235  	}
   236  
   237  	defer func() {
   238  		closeErr := res.Body.Close()
   239  		if err == nil && closeErr != nil {
   240  			err = closeErr
   241  		}
   242  	}()
   243  
   244  	if res.IsError() {
   245  		return fmt.Errorf("indexing of '%s' failed: %s", index, res.String())
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func (p *P) setDefaults() {
   252  	if p.Version == "" {
   253  		p.Version = defaultVersion
   254  	}
   255  }