github.com/etecs-ru/gnomock@v0.13.2/preset/rabbitmq/preset.go (about) 1 // Package rabbitmq provides a Gnomock Preset for RabbitMQ. 2 package rabbitmq 3 4 import ( 5 "context" 6 "crypto/tls" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "os" 13 "strings" 14 15 "github.com/etecs-ru/gnomock" 16 "github.com/etecs-ru/gnomock/internal/registry" 17 "github.com/streadway/amqp" 18 ) 19 20 // ManagementPort is a name of the port exposed by RabbitMQ management plugin. 21 // This port is only available when an appropriate version of RabbitMQ docker 22 // image is used. See `Preset` docs for more info. 23 const ManagementPort = "management" 24 25 const ( 26 defaultUser = "guest" 27 defaultPassword = "guest" 28 defaultVersion = "3.8.9" 29 defaultPort = 5672 30 managementPort = 15672 31 ) 32 33 // Message is a single message sent to RabbitMQ. 34 type Message struct { 35 Queue string `json:"queue"` 36 ContentType string `json:"content_type"` 37 StringBody string `json:"string_body"` 38 Body []byte `json:"body"` 39 } 40 41 func init() { 42 registry.Register("rabbitmq", func() gnomock.Preset { return &P{} }) 43 } 44 45 // Preset creates a new Gmomock RabbitMQ preset. This preset includes a 46 // RabbitMQ specific healthcheck function and default RabbitMQ image and port. 47 // 48 // By default, this preset does not use RabbitMQ Management plugin. To enable 49 // it, use one of the management tags with `WithVersion` option. Management 50 // port will be accessible using `container.Port(rabbitmq.ManagementPort)`. See 51 // https://hub.docker.com/_/rabbitmq/?tab=tags for a list of available tags. 52 // 53 // When used without specifying username/password, default ones are used: 54 // guest/guest. Default version for this preset is 3.8.9. 55 func Preset(opts ...Option) gnomock.Preset { 56 p := &P{} 57 58 for _, opt := range opts { 59 opt(p) 60 } 61 62 return p 63 } 64 65 // P is a Gnomock Preset implementation of RabbitMQ. 66 type P struct { 67 User string `json:"user"` 68 Password string `json:"password"` 69 Version string `json:"version"` 70 Messages []Message `json:"messages"` 71 MessagesFiles []string `json:"messages_files"` 72 } 73 74 // Image returns an image that should be pulled to create this container. 75 func (p *P) Image() string { 76 return fmt.Sprintf("docker.io/library/rabbitmq:%s", p.Version) 77 } 78 79 // Ports returns ports that should be used to access this container. 80 func (p *P) Ports() gnomock.NamedPorts { 81 namedPorts := gnomock.DefaultTCP(defaultPort) 82 83 if p.isManagement() { 84 namedPorts[ManagementPort] = gnomock.Port{Protocol: "tcp", Port: managementPort} 85 } 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 } 97 98 if p.User != "" && p.Password != "" { 99 opts = append( 100 opts, 101 gnomock.WithEnv("RABBITMQ_DEFAULT_USER="+p.User), 102 gnomock.WithEnv("RABBITMQ_DEFAULT_PASS="+p.Password), 103 ) 104 } 105 106 if len(p.Messages)+len(p.MessagesFiles) > 0 { 107 opts = append(opts, gnomock.WithInit(p.initf)) 108 } 109 110 return opts 111 } 112 113 func (p *P) healthcheck(ctx context.Context, c *gnomock.Container) error { 114 conn, err := p.connect(c) 115 if err != nil { 116 return fmt.Errorf("connection failed: %w", err) 117 } 118 119 err = conn.Close() 120 if err != nil { 121 return fmt.Errorf("can't close connection: %w", err) 122 } 123 124 if p.isManagement() { 125 addr := c.Address(ManagementPort) 126 url := fmt.Sprintf("http://%s/api/overview", addr) 127 128 client := &http.Client{ 129 Transport: &http.Transport{ 130 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint:gosec // allow for tests 131 }, 132 } 133 134 // any non-err response is valid, it is most likely 401 Unauthorized 135 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 136 if err != nil { 137 return err 138 } 139 140 resp, err := client.Do(req) 141 if err != nil { 142 return err 143 } 144 145 _ = resp.Body.Close() 146 } 147 148 return nil 149 } 150 151 func (p *P) setDefaults() { 152 if p.Version == "" { 153 p.Version = defaultVersion 154 } 155 156 if p.User == "" && p.Password == "" { 157 p.User = defaultUser 158 p.Password = defaultPassword 159 } 160 } 161 162 func (p *P) initf(ctx context.Context, c *gnomock.Container) (err error) { 163 conn, err := p.connect(c) 164 if err != nil { 165 return fmt.Errorf("can't connect to rabbitmq: %w", err) 166 } 167 168 defer func() { 169 closeErr := conn.Close() 170 if err == nil && closeErr != nil { 171 err = closeErr 172 } 173 }() 174 175 if err := p.loadFiles(); err != nil { 176 return err 177 } 178 179 messagesByQueue := make(map[string][]Message) 180 for _, m := range p.Messages { 181 messagesByQueue[m.Queue] = append(messagesByQueue[m.Queue], m) 182 } 183 184 queues := make([]string, len(messagesByQueue)) 185 for q := range messagesByQueue { 186 queues = append(queues, q) 187 } 188 189 ch, err := conn.Channel() 190 if err != nil { 191 return fmt.Errorf("can't open channel: %w", err) 192 } 193 194 defer func() { 195 closeErr := ch.Close() 196 if err == nil && closeErr != nil { 197 err = closeErr 198 } 199 }() 200 201 if err := declareQueues(ch, queues); err != nil { 202 return err 203 } 204 205 for queue, messages := range messagesByQueue { 206 if err := p.sendMessagesIntoQueue(ch, queue, messages); err != nil { 207 return fmt.Errorf("can't send messages into queue '%s': %w", queue, err) 208 } 209 } 210 211 return nil 212 } 213 214 func (p *P) loadFiles() error { 215 if len(p.MessagesFiles) > 0 { 216 for _, fName := range p.MessagesFiles { 217 msgs, err := p.loadMessagesFromFile(fName) 218 if err != nil { 219 return fmt.Errorf("can't read messages from file '%s': %w", fName, err) 220 } 221 222 p.Messages = append(p.Messages, msgs...) 223 } 224 } 225 226 return nil 227 } 228 229 func declareQueues(ch *amqp.Channel, qs []string) error { 230 for _, queue := range qs { 231 _, err := ch.QueueDeclare(queue, false, false, false, false, nil) 232 if err != nil { 233 return fmt.Errorf("can't open queue '%s': %w", queue, err) 234 } 235 } 236 237 return nil 238 } 239 240 func (p *P) isManagement() bool { 241 return strings.Contains(p.Version, "management") 242 } 243 244 // nolint:gosec 245 func (p *P) loadMessagesFromFile(fName string) (msgs []Message, err error) { 246 f, err := os.Open(fName) 247 if err != nil { 248 return nil, fmt.Errorf("can't open messages file '%s': %w", fName, err) 249 } 250 251 defer func() { 252 closeErr := f.Close() 253 if err == nil && closeErr != nil { 254 err = closeErr 255 } 256 }() 257 258 decoder := json.NewDecoder(f) 259 260 for { 261 var m Message 262 263 err = decoder.Decode(&m) 264 if errors.Is(err, io.EOF) { 265 break 266 } 267 268 if err != nil { 269 return nil, fmt.Errorf("can't read message from file '%s': %w", fName, err) 270 } 271 272 msgs = append(msgs, m) 273 } 274 275 return msgs, nil 276 } 277 278 func (p *P) connect(c *gnomock.Container) (*amqp.Connection, error) { 279 return amqp.Dial(fmt.Sprintf("amqp://%s:%s@%s:%d", p.User, p.Password, c.Host, c.DefaultPort())) 280 } 281 282 func (p *P) sendMessagesIntoQueue(ch *amqp.Channel, q string, msgs []Message) (err error) { 283 for _, m := range msgs { 284 var body []byte 285 if m.Body != nil { 286 body = m.Body 287 } else { 288 body = []byte(m.StringBody) 289 } 290 291 if err := ch.Publish( 292 "", 293 q, 294 false, 295 false, 296 amqp.Publishing{ 297 ContentType: m.ContentType, 298 Body: body, 299 }, 300 ); err != nil { 301 return fmt.Errorf("publish message failed: %w", err) 302 } 303 } 304 305 return nil 306 }