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  }