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 }