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 }