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

     1  // Package kafka provides a Gnomock Preset for Kafka.
     2  package kafka
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/etecs-ru/gnomock"
    14  	"github.com/etecs-ru/gnomock/internal/registry"
    15  	"github.com/segmentio/kafka-go"
    16  )
    17  
    18  // The following ports are exposed by this preset:
    19  const (
    20  	BrokerPort    = "broker"
    21  	ZooKeeperPort = "zookeeper"
    22  	WebPort       = "web"
    23  )
    24  
    25  const (
    26  	defaultVersion = "2.5.1-L0"
    27  	brokerPort     = 49092
    28  	zookeeperPort  = 2181
    29  	webPort        = 3030
    30  )
    31  
    32  // Message is a single message sent to Kafka.
    33  type Message struct {
    34  	Topic string `json:"topic"`
    35  	Key   string `json:"key"`
    36  	Value string `json:"value"`
    37  	Time  int64  `json:"time"`
    38  }
    39  
    40  func init() {
    41  	registry.Register("kafka", func() gnomock.Preset { return &P{} })
    42  }
    43  
    44  // Preset creates a new Gmomock Kafka preset. This preset includes a
    45  // Kafka specific healthcheck function and default Kafka image and ports.
    46  //
    47  // Kafka preset uses a constant broker port number (49092) instead of
    48  // allocating a random unoccupied port on every run. Please make sure this port
    49  // is available when using this preset.
    50  //
    51  // By default, this preset uses `lensesio/fast-data-dev` docker image with
    52  // version `2.5.1-L0` (version can be changed using `WithVersion`).
    53  func Preset(opts ...Option) gnomock.Preset {
    54  	p := &P{}
    55  
    56  	for _, opt := range opts {
    57  		opt(p)
    58  	}
    59  
    60  	return p
    61  }
    62  
    63  // P is a Gnomock Preset implementation of Kafka.
    64  type P struct {
    65  	Version       string    `json:"version"`
    66  	Topics        []string  `json:"topics"`
    67  	Messages      []Message `json:"messages"`
    68  	MessagesFiles []string  `json:"messages_files"`
    69  }
    70  
    71  // Image returns an image that should be pulled to create this container.
    72  func (p *P) Image() string {
    73  	return fmt.Sprintf("docker.io/lensesio/fast-data-dev:%s", p.Version)
    74  }
    75  
    76  // Ports returns ports that should be used to access this container.
    77  func (p *P) Ports() gnomock.NamedPorts {
    78  	namedPorts := make(gnomock.NamedPorts, 3)
    79  
    80  	bp := gnomock.TCP(brokerPort)
    81  	bp.HostPort = brokerPort
    82  	namedPorts[BrokerPort] = bp
    83  
    84  	namedPorts[ZooKeeperPort] = gnomock.TCP(zookeeperPort)
    85  	namedPorts[WebPort] = gnomock.TCP(webPort)
    86  
    87  	return namedPorts
    88  }
    89  
    90  // Options returns a list of options to configure this container.
    91  func (p *P) Options() []gnomock.Option {
    92  	p.setDefaults()
    93  
    94  	opts := []gnomock.Option{
    95  		gnomock.WithHealthCheck(p.healthcheck),
    96  		gnomock.WithEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE=true"),
    97  		gnomock.WithEnv("ADV_HOST=127.0.0.1"),
    98  		gnomock.WithEnv(fmt.Sprintf("BROKER_PORT=%d", brokerPort)),
    99  		gnomock.WithEnv("RUNTESTS=0"),
   100  		gnomock.WithEnv("RUNNING_SAMPLEDATA=0"),
   101  		gnomock.WithEnv("SAMPLEDATA=0"),
   102  	}
   103  
   104  	if len(p.Topics) > 0 || len(p.Messages) > 0 {
   105  		opts = append(opts, gnomock.WithInit(p.initf))
   106  	}
   107  
   108  	return opts
   109  }
   110  
   111  func (p *P) healthcheck(ctx context.Context, c *gnomock.Container) (err error) {
   112  	conn, err := p.connect(c)
   113  	if err != nil {
   114  		return fmt.Errorf("can't connect to kafka: %w", err)
   115  	}
   116  
   117  	defer func() {
   118  		closeErr := conn.Close()
   119  		if err == nil && closeErr != nil {
   120  			err = closeErr
   121  		}
   122  	}()
   123  
   124  	if _, err := conn.ApiVersions(); err != nil {
   125  		return fmt.Errorf("can't get version info: %w", err)
   126  	}
   127  
   128  	if err := conn.CreateTopics(kafka.TopicConfig{
   129  		Topic:             "gnomock",
   130  		ReplicationFactor: 1,
   131  		NumPartitions:     1,
   132  	}); err != nil {
   133  		return fmt.Errorf("can't create topic: %w", err)
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  func (p *P) setDefaults() {
   140  	if p.Version == "" {
   141  		p.Version = defaultVersion
   142  	}
   143  }
   144  
   145  func (p *P) initf(ctx context.Context, c *gnomock.Container) (err error) {
   146  	conn, err := p.connect(c)
   147  	if err != nil {
   148  		return fmt.Errorf("can't connect to kafka: %w", err)
   149  	}
   150  
   151  	defer func() {
   152  		closeErr := conn.Close()
   153  		if err == nil && closeErr != nil {
   154  			err = closeErr
   155  		}
   156  	}()
   157  
   158  	if len(p.MessagesFiles) > 0 {
   159  		for _, fName := range p.MessagesFiles {
   160  			msgs, err := p.loadMessagesFromFile(fName)
   161  			if err != nil {
   162  				return fmt.Errorf("can't read messages from file '%s': %w", fName, err)
   163  			}
   164  
   165  			p.Messages = append(p.Messages, msgs...)
   166  		}
   167  	}
   168  
   169  	messagesByTopics := make(map[string][]Message)
   170  
   171  	for _, m := range p.Messages {
   172  		messagesByTopics[m.Topic] = append(messagesByTopics[m.Topic], m)
   173  	}
   174  
   175  	for topic := range messagesByTopics {
   176  		p.Topics = append(p.Topics, topic)
   177  	}
   178  
   179  	topics := make([]kafka.TopicConfig, 0, len(p.Topics))
   180  
   181  	for _, topic := range p.Topics {
   182  		topics = append(topics, kafka.TopicConfig{
   183  			Topic:             topic,
   184  			ReplicationFactor: 1,
   185  			NumPartitions:     1,
   186  		})
   187  	}
   188  
   189  	if err := conn.CreateTopics(topics...); err != nil {
   190  		return fmt.Errorf("can't create topics: %w", err)
   191  	}
   192  
   193  	for topic, messages := range messagesByTopics {
   194  		if err := p.sendMessagesIntoTopic(ctx, c, topic, messages); err != nil {
   195  			return fmt.Errorf("can't send messages into topic '%s': %w", topic, err)
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  // nolint:gosec
   203  func (p *P) loadMessagesFromFile(fName string) (msgs []Message, err error) {
   204  	f, err := os.Open(fName)
   205  	if err != nil {
   206  		return nil, fmt.Errorf("can't open messages file '%s': %w", fName, err)
   207  	}
   208  
   209  	defer func() {
   210  		closeErr := f.Close()
   211  		if err == nil && closeErr != nil {
   212  			err = closeErr
   213  		}
   214  	}()
   215  
   216  	decoder := json.NewDecoder(f)
   217  
   218  	for {
   219  		var m Message
   220  
   221  		err = decoder.Decode(&m)
   222  		if errors.Is(err, io.EOF) {
   223  			break
   224  		}
   225  
   226  		if err != nil {
   227  			return nil, fmt.Errorf("can't read message from file '%s': %w", fName, err)
   228  		}
   229  
   230  		msgs = append(msgs, m)
   231  	}
   232  
   233  	return msgs, nil
   234  }
   235  
   236  func (p *P) connect(c *gnomock.Container) (*kafka.Conn, error) {
   237  	return kafka.Dial("tcp", c.Address(BrokerPort))
   238  }
   239  
   240  // nolint: lll
   241  func (p *P) sendMessagesIntoTopic(ctx context.Context, c *gnomock.Container, topic string, messages []Message) (err error) {
   242  	w := kafka.NewWriter(kafka.WriterConfig{
   243  		Brokers:  []string{c.Address(BrokerPort)},
   244  		Topic:    topic,
   245  		Balancer: &kafka.LeastBytes{},
   246  	})
   247  
   248  	defer func() {
   249  		closeErr := w.Close()
   250  		if err == nil && closeErr != nil {
   251  			err = closeErr
   252  		}
   253  	}()
   254  
   255  	kafkaMessages := make([]kafka.Message, len(messages))
   256  
   257  	for i, m := range messages {
   258  		kafkaMessages[i] = kafka.Message{
   259  			Key:   []byte(m.Key),
   260  			Value: []byte(m.Value),
   261  			Time:  time.Unix(0, m.Time),
   262  		}
   263  	}
   264  
   265  	if err := w.WriteMessages(ctx, kafkaMessages...); err != nil {
   266  		return fmt.Errorf("write messages failed: %w", err)
   267  	}
   268  
   269  	return nil
   270  }