├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerfile-test ├── LICENSE ├── Makefile ├── README.md ├── config └── config.go ├── connector ├── connector.go ├── connector_suite_test.go └── connector_test.go ├── consumer └── consumer.go ├── docker-compose.yml ├── examples ├── rabbit_to_lambda.json ├── rabbit_to_sns.json └── rabbit_to_sqs.json ├── forwarder └── forwarder.go ├── go.mod ├── go.sum ├── img └── rabbit-amazon-forwarder.png ├── lambda ├── forwarder.go └── forwarder_test.go ├── mapping ├── mapping.go └── mapping_test.go ├── rabbitmq └── consumer.go ├── server.go ├── sns ├── forwarder.go └── forwarder_test.go ├── sqs ├── forwarder.go └── forwarder_test.go ├── supervisor ├── supervisor.go └── supervisor_test.go └── tests ├── rabbit_to_sns.json └── rabbit_to_sqs.json /.dockerignore: -------------------------------------------------------------------------------- 1 | samples 2 | .git 3 | Dockerfile* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rabbit-amazon-forwarder 2 | samples 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - docker-compose run --rm tests 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine AS golang-build 2 | 3 | RUN mkdir -p /go/src/github.com/AirHelp/rabbit-amazon-forwarder 4 | WORKDIR /go/src/github.com/AirHelp/rabbit-amazon-forwarder 5 | 6 | RUN apk --no-cache add git 7 | 8 | COPY . . 9 | RUN go mod tidy -go=1.18 -compat=1.18 10 | 11 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o rabbit-amazon-forwarder . 12 | 13 | FROM alpine 14 | 15 | RUN mkdir -p /config 16 | RUN mkdir -p /certs 17 | RUN apk --update upgrade && \ 18 | apk add curl ca-certificates && \ 19 | update-ca-certificates && \ 20 | rm -rf /var/cache/apk/* 21 | 22 | COPY --from=golang-build /go/src/github.com/AirHelp/rabbit-amazon-forwarder/rabbit-amazon-forwarder / 23 | CMD ["/rabbit-amazon-forwarder"] 24 | -------------------------------------------------------------------------------- /Dockerfile-test: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine 2 | 3 | WORKDIR /go/src/github.com/AirHelp/rabbit-amazon-forwarder 4 | 5 | RUN apk --no-cache add git gcc musl-dev 6 | 7 | COPY go.mod go.sum ./ 8 | RUN go mod tidy -go=1.18 -compat=1.18 9 | 10 | COPY . . 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 AirHelp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t airhelp/rabbit-amazon-forwarder -f Dockerfile . 3 | 4 | push: test build 5 | docker push airhelp/rabbit-amazon-forwarder 6 | 7 | test: 8 | docker-compose run --rm tests 9 | 10 | dev: 11 | go build 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ -> Amazon forwarder 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/AirHelp/rabbit-amazon-forwarder)](https://goreportcard.com/report/github.com/AirHelp/rabbit-amazon-forwarder) 4 | 5 | Application to forward messages from RabbitMQ to different Amazon services. 6 | 7 | Key features: 8 | 9 | * forwarding RabbitMQ message to AWS SNS topic 10 | * forwarding RabbitMQ message to AWS SNS queue 11 | * triggering AWS lambda function directly from RabbitMQ message 12 | * automatic RabbitMQ reconnect 13 | * message delivery assurance based on RabbitMQ persistency and AWS error handling 14 | * dedicated dead-letter exchange and queue creation 15 | * http health checks and restart functionality 16 | 17 | ## Architecture 18 | 19 | ![Alt text](img/rabbit-amazon-forwarder.png?raw=true "RabbitMQ -> Amazon architecture") 20 | 21 | ## Configuration 22 | 23 | The list of RabbitMQ sources and corresponding AWS target resources are stored in mapping file. 24 | 25 | ### Mapping file 26 | 27 | Sample of RabbitMQ -> SNS mapping file. All fields are required. Samples are located in [examples](https://github.com/AirHelp/rabbit-amazon-forwarder/tree/master/examples) directory. 28 | ```json 29 | [ 30 | { 31 | "source" : { 32 | "type" : "RabbitMQ", 33 | "name" : "test-rabbit", 34 | "connection" : "amqp://guest:guest@localhost:5672/", 35 | "topic" : "amq.topic", 36 | "queue" : "test-queue", 37 | "routingKeys" : ["#"] 38 | }, 39 | "destination" : { 40 | "type" : "SNS", 41 | "name" : "test-sns", 42 | "target" : "arn:aws:sns:eu-west-1:XXXXXXXX:test-forwarder" 43 | } 44 | } 45 | ] 46 | ``` 47 | 48 | ### Environment variables 49 | 50 | Forwarder uses the following environment variables: 51 | ```bash 52 | export MAPPING_FILE=/config/mapping.json 53 | export AWS_REGION=region 54 | export AWS_ACCESS_KEY_ID=access_key 55 | export AWS_SECRET_ACCESS_KEY=secret_key 56 | ``` 57 | 58 | #### Using TLS with rabbit 59 | 60 | Specify amqps for the rabbit connection ub the mapping file: 61 | ``` 62 | "connection" : "amqps://guest:guest@localhost:5671/", 63 | ``` 64 | 65 | Additional environment variables for working with TLS and rabbit: 66 | ``` 67 | export CA_CERT=/certs/ca_certificate.pem 68 | export CERT_FILE=/certs/client_certificate.pem 69 | export KEY_FILE=/certs/client_key.pem 70 | ``` 71 | 72 | ### Amazon configuration 73 | 74 | When making subscription to SNS -> SQS/HTTP/HTTPS set `Raw message delivery` to ensure that json messages are not escaped. 75 | 76 | ## Build docker image 77 | 78 | ```bash 79 | make build 80 | ``` 81 | 82 | ## Run 83 | 84 | Using docker: 85 | ```bash 86 | docker run \ 87 | -e AWS_REGION=$AWS_REGION \ 88 | -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ 89 | -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 90 | -e MAPPING_FILE=/config/mapping.json \ 91 | -v $MAPPING_FILE:/config/mapping.json \ 92 | -p 8080:8080 \ 93 | airhelp/rabbit-amazon-forwarder 94 | ``` 95 | 96 | Using docker-compose: 97 | ```bash 98 | docker-compose up 99 | ``` 100 | 101 | ## Test 102 | ``` 103 | docker-compose build --pull 104 | docker-compose run --rm tests 105 | ``` 106 | 107 | # Release 108 | 109 | ```bash 110 | make push 111 | docker tag airhelp/rabbit-amazon-forwarder airhelp/rabbit-amazon-forwarder:$VERSION 112 | docker push airhelp/rabbit-amazon-forwarder:$VERSION 113 | ``` 114 | 115 | ## Supervisor 116 | 117 | Supervisor is a module which starts the consumer->forwarder pairs. 118 | Exposed endpoints: 119 | - `APP_URL/health` - returns status if all consumers are running 120 | - `APP_URL/restart` - restarts all consumer->forwarder pairs 121 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | // MappingFile mapping file environment variable 5 | MappingFile = "MAPPING_FILE" 6 | CaCertFile = "CA_CERT_FILE" 7 | CertFile = "CERT_FILE" 8 | KeyFile = "KEY_FILE" 9 | ) 10 | 11 | // RabbitEntry RabbitMQ mapping entry 12 | type RabbitEntry struct { 13 | Type string `json:"type"` 14 | Name string `json:"name"` 15 | ConnectionURL string `json:"connection"` 16 | ExchangeName string `json:"topic"` 17 | QueueName string `json:"queue"` 18 | RoutingKey string `json:"routing"` 19 | RoutingKeys []string `json:"routingKeys"` 20 | } 21 | 22 | // AmazonEntry SQS/SNS mapping entry 23 | type AmazonEntry struct { 24 | Type string `json:"type"` 25 | Name string `json:"name"` 26 | Target string `json:"target"` 27 | } 28 | -------------------------------------------------------------------------------- /connector/connector.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/streadway/amqp" 14 | ) 15 | 16 | type FileReader interface { 17 | ReadFile(filename string) ([]byte, error) 18 | } 19 | 20 | type IOFileReader struct { 21 | } 22 | 23 | func (i *IOFileReader) ReadFile(filename string) ([]byte, error) { 24 | return ioutil.ReadFile(filename) 25 | } 26 | 27 | type CertPoolMaker interface { 28 | NewCertPoolWithAppendedCa(caCert []byte) *x509.CertPool 29 | } 30 | 31 | type X509CertPoolMaker struct { 32 | } 33 | 34 | func (x *X509CertPoolMaker) NewCertPoolWithAppendedCa(caCert []byte) *x509.CertPool { 35 | certPool := x509.NewCertPool() 36 | certPool.AppendCertsFromPEM(caCert) 37 | return certPool 38 | } 39 | 40 | type KeyLoader interface { 41 | LoadKeyPair(certFile, keyFile string) (tls.Certificate, error) 42 | } 43 | 44 | type X509KeyPairLoader struct { 45 | } 46 | 47 | func (x *X509KeyPairLoader) LoadKeyPair(certFile string, keyFile string) (tls.Certificate, error) { 48 | return tls.LoadX509KeyPair(certFile, keyFile) 49 | } 50 | 51 | type RabbitDialer interface { 52 | Dial(connectionURL string) (*amqp.Connection, error) 53 | } 54 | 55 | type BasicRabbitDialer struct { 56 | } 57 | 58 | func (s *BasicRabbitDialer) Dial(connectionURL string) (*amqp.Connection, error) { 59 | return amqp.Dial(connectionURL) 60 | } 61 | 62 | type TlsRabbitDialer interface { 63 | DialTLS(connectionURL string, tlsConfig *tls.Config) (*amqp.Connection, error) 64 | } 65 | 66 | type X509TlsDialer struct { 67 | } 68 | 69 | func (s *X509TlsDialer) DialTLS(connectionURL string, tlsConfig *tls.Config) (*amqp.Connection, error) { 70 | return amqp.DialTLS(connectionURL, tlsConfig) 71 | } 72 | 73 | type RabbitConnector interface { 74 | CreateConnection(connectionURL string) (*amqp.Connection, error) 75 | } 76 | 77 | type BasicRabbitConnector struct { 78 | BasicRabbitDialer RabbitDialer 79 | } 80 | 81 | func (c *BasicRabbitConnector) CreateConnection(connectionURL string) (*amqp.Connection, error) { 82 | log.Info("Dialing in") 83 | return c.BasicRabbitDialer.Dial(connectionURL) 84 | } 85 | 86 | type TlsRabbitConnector struct { 87 | TlsConfig *tls.Config 88 | FileReader FileReader 89 | CertPoolMaker CertPoolMaker 90 | KeyLoader KeyLoader 91 | TlsDialer TlsRabbitDialer 92 | } 93 | 94 | func (c *TlsRabbitConnector) CreateConnection(connectionURL string) (*amqp.Connection, error) { 95 | log.Info("Dialing in via TLS") 96 | caCertFilePath := os.Getenv(config.CaCertFile) 97 | 98 | if ca, err := c.FileReader.ReadFile(caCertFilePath); err == nil { 99 | c.TlsConfig.RootCAs = c.CertPoolMaker.NewCertPoolWithAppendedCa(ca) 100 | } else { 101 | log.WithFields(log.Fields{ 102 | "error": err.Error(), 103 | config.CaCertFile: caCertFilePath}).Info("Error loading CA Cert file") 104 | return nil, err 105 | } 106 | 107 | certFilePath := os.Getenv(config.CertFile) 108 | keyFilePath := os.Getenv(config.KeyFile) 109 | if _, err := c.KeyLoader.LoadKeyPair(certFilePath, keyFilePath); err == nil { 110 | c.TlsConfig.Certificates = append(c.TlsConfig.Certificates, cert) 111 | } else { 112 | log.WithFields(log.Fields{ 113 | "error": err.Error(), 114 | config.CertFile: certFilePath, 115 | config.KeyFile: keyFilePath}).Info("Error loading client certificates") 116 | } 117 | return c.TlsDialer.DialTLS(connectionURL, c.TlsConfig) 118 | } 119 | 120 | func CreateBasicRabbitConnector() *BasicRabbitConnector { 121 | return &BasicRabbitConnector{ 122 | BasicRabbitDialer: &BasicRabbitDialer{}, 123 | } 124 | } 125 | 126 | func CreateTlsRabbitConnector() *TlsRabbitConnector { 127 | return &TlsRabbitConnector{ 128 | TlsConfig: new(tls.Config), 129 | FileReader: &IOFileReader{}, 130 | CertPoolMaker: &X509CertPoolMaker{}, 131 | KeyLoader: &X509KeyPairLoader{}, 132 | TlsDialer: &X509TlsDialer{}, 133 | } 134 | } 135 | 136 | func CreateConnector(connectionURL string) RabbitConnector { 137 | if strings.HasPrefix(connectionURL, "amqps") { 138 | return CreateTlsRabbitConnector() 139 | } else { 140 | return CreateBasicRabbitConnector() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /connector/connector_suite_test.go: -------------------------------------------------------------------------------- 1 | package connector_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestConnector(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Connector Suite") 13 | } 14 | -------------------------------------------------------------------------------- /connector/connector_test.go: -------------------------------------------------------------------------------- 1 | package connector_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "os" 8 | 9 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 10 | "github.com/AirHelp/rabbit-amazon-forwarder/connector" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/streadway/amqp" 14 | ) 15 | 16 | var _ = Describe("Connector", func() { 17 | 18 | Describe("Creating connectors", func() { 19 | Context("With a basic rabbit configuration", func() { 20 | It("should be a BasicRabbitConnector", func() { 21 | actualConnect := connector.CreateConnector("amqp") 22 | Expect(actualConnect).Should(BeAssignableToTypeOf(&connector.BasicRabbitConnector{})) 23 | }) 24 | }) 25 | 26 | Context("With an amqps value somewhere else in the connection url", func() { 27 | It("should be a BasicRabbitConnector", func() { 28 | actualConnect := connector.CreateConnector("amqp://guest:guest@rabbbit-amqps:5672") 29 | Expect(actualConnect).Should(BeAssignableToTypeOf(&connector.BasicRabbitConnector{})) 30 | }) 31 | }) 32 | 33 | Context("With a tls rabbit configuration", func() { 34 | It("should be a TlsRabbitConnector", func() { 35 | actualConnect := connector.CreateConnector("amqps") 36 | Expect(actualConnect).Should(BeAssignableToTypeOf(&connector.TlsRabbitConnector{})) 37 | }) 38 | }) 39 | }) 40 | 41 | Describe("Connecting a basic rabbit connector", func() { 42 | 43 | var ( 44 | rabbitConnector connector.RabbitConnector 45 | dialer *MockBasicRabbitDialer 46 | ) 47 | 48 | BeforeEach(func() { 49 | dialer = &MockBasicRabbitDialer{} 50 | rabbitConnector = createBasicConnector(dialer) 51 | }) 52 | 53 | Context("With no problems creating the connection", func() { 54 | It("Should create a connection", func() { 55 | expectedConnection := createDummyAmqpConnection() 56 | dialer.ReturnedConnection = expectedConnection 57 | 58 | connection, err := rabbitConnector.CreateConnection("any amqp url") 59 | 60 | Expect(connection).Should(Equal(expectedConnection)) 61 | Expect(err).Should(BeNil()) 62 | }) 63 | }) 64 | 65 | Context("With an error creating the connection", func() { 66 | It("Should return an error", func() { 67 | dialer.Error = errors.New("Expected") 68 | dialer.ReturnedConnection = nil 69 | 70 | connection, err := rabbitConnector.CreateConnection("any amqp url") 71 | 72 | Expect(connection).Should(BeNil()) 73 | Expect(err).Should(Equal(dialer.Error)) 74 | }) 75 | }) 76 | }) 77 | 78 | Describe("Connecting a tls rabbit connector", func() { 79 | 80 | var ( 81 | rabbitConnector connector.RabbitConnector 82 | fileReader *MockFileReader 83 | dialer *MockTlsRabbitDialer 84 | tlsConfig *tls.Config 85 | certPoolMaker *MockCertPoolMaker 86 | keyLoader *MockkeyLoader 87 | ) 88 | 89 | BeforeEach(func() { 90 | os.Setenv(config.CaCertFile, "CaName") 91 | os.Setenv(config.CertFile, "CertFile") 92 | os.Setenv(config.KeyFile, "KeyFile") 93 | 94 | dialer = &MockTlsRabbitDialer{} 95 | fileReader = &MockFileReader{ 96 | Error: nil, 97 | DummyFile: []byte("Dummy file"), 98 | } 99 | tlsConfig = new(tls.Config) 100 | certPoolMaker = &MockCertPoolMaker{ 101 | CertPoolToReturn: x509.NewCertPool(), 102 | } 103 | keyLoader = &MockkeyLoader{ 104 | ReturnedCertificate: tls.Certificate{}, 105 | } 106 | rabbitConnector = createTlsConnector(dialer, fileReader, tlsConfig, certPoolMaker, keyLoader) 107 | }) 108 | 109 | Context("With no problems creating the connection", func() { 110 | It("Should create a connection", func() { 111 | expectedConnection := createDummyAmqpConnection() 112 | dialer.ReturnedConnection = expectedConnection 113 | 114 | connection, err := rabbitConnector.CreateConnection("any amqps url") 115 | 116 | // assert that file reader loaded the 117 | Expect(fileReader.FileNameRead).Should(Equal("CaName")) 118 | 119 | // asert that ca is added to root ca 120 | Expect(certPoolMaker.AppendedCaCert).Should(Equal([]byte("Dummy file"))) 121 | Expect(tlsConfig.RootCAs).Should(Equal(certPoolMaker.CertPoolToReturn)) 122 | 123 | // assert that client certifcate is added 124 | Expect(keyLoader.CertFileProvided).Should(Equal("CertFile")) 125 | Expect(keyLoader.KeyFileProvided).Should(Equal("KeyFile")) 126 | Expect(tlsConfig.Certificates).Should(ContainElement(keyLoader.ReturnedCertificate)) 127 | 128 | //assert that connection is created with correct params 129 | Expect(dialer.ConnectionUrlProvided).Should(Equal("any amqps url")) 130 | Expect(dialer.TlsConfigProvided).Should(Equal(tlsConfig)) 131 | 132 | //assert that the connection is returned 133 | Expect(connection).Should(Equal(expectedConnection)) 134 | Expect(err).Should(BeNil()) 135 | }) 136 | }) 137 | 138 | Context("With an error loading the ca certificate", func() { 139 | It("Should return an error", func() { 140 | fileReader.Error = errors.New("Expected") 141 | fileReader.DummyFile = nil 142 | 143 | connection, err := rabbitConnector.CreateConnection("any amqp url") 144 | 145 | Expect(connection).Should(BeNil()) 146 | Expect(err).Should(Equal(fileReader.Error)) 147 | }) 148 | }) 149 | 150 | Context("With an error loading client certificates", func() { 151 | It("Should proceed with creating the connection", func() { 152 | // We can leave the error handling to the TLS protocol 153 | // and log an error indicating that no keys were loaded 154 | var nilCertificate tls.Certificate 155 | expectedConnection := createDummyAmqpConnection() 156 | dialer.ReturnedConnection = expectedConnection 157 | keyLoader.ReturnedCertificate = nilCertificate 158 | keyLoader.Error = errors.New("Expected") 159 | 160 | connection, err := rabbitConnector.CreateConnection("any amqps url") 161 | 162 | // assert that client certifcate is added 163 | Expect(len(tlsConfig.Certificates)).Should(Equal(0)) 164 | 165 | //assert that connection is created with correct params 166 | Expect(dialer.ConnectionUrlProvided).Should(Equal("any amqps url")) 167 | Expect(dialer.TlsConfigProvided).Should(Equal(tlsConfig)) 168 | 169 | //assert that the connection is returned 170 | Expect(connection).Should(Equal(expectedConnection)) 171 | Expect(err).Should(BeNil()) 172 | }) 173 | }) 174 | }) 175 | }) 176 | 177 | func createBasicConnector(mockDialer connector.RabbitDialer) *connector.BasicRabbitConnector { 178 | return &connector.BasicRabbitConnector{ 179 | BasicRabbitDialer: mockDialer, 180 | } 181 | } 182 | 183 | func createTlsConnector( 184 | mockDialer connector.TlsRabbitDialer, 185 | mockFileReader connector.FileReader, 186 | tlsConfig *tls.Config, 187 | certPoolMaker connector.CertPoolMaker, 188 | keyLoader connector.KeyLoader) *connector.TlsRabbitConnector { 189 | return &connector.TlsRabbitConnector{ 190 | TlsConfig: tlsConfig, 191 | FileReader: mockFileReader, 192 | CertPoolMaker: certPoolMaker, 193 | KeyLoader: keyLoader, 194 | TlsDialer: mockDialer, 195 | } 196 | } 197 | 198 | type MockFileReader struct { 199 | FileNameRead string 200 | Error error 201 | DummyFile []byte 202 | } 203 | 204 | func (i *MockFileReader) ReadFile(filename string) ([]byte, error) { 205 | i.FileNameRead = filename 206 | return i.DummyFile, i.Error 207 | } 208 | 209 | type MockkeyLoader struct { 210 | CertFileProvided string 211 | KeyFileProvided string 212 | ReturnedCertificate tls.Certificate 213 | Error error 214 | } 215 | 216 | func (x *MockkeyLoader) LoadKeyPair(certFile string, keyFile string) (tls.Certificate, error) { 217 | x.CertFileProvided = certFile 218 | x.KeyFileProvided = keyFile 219 | return x.ReturnedCertificate, x.Error 220 | } 221 | 222 | type MockTlsRabbitDialer struct { 223 | ConnectionUrlProvided string 224 | TlsConfigProvided *tls.Config 225 | ReturnedConnection *amqp.Connection 226 | Error error 227 | } 228 | 229 | func (s *MockTlsRabbitDialer) DialTLS(connectionURL string, tlsConfig *tls.Config) (*amqp.Connection, error) { 230 | s.ConnectionUrlProvided = connectionURL 231 | s.TlsConfigProvided = tlsConfig 232 | return s.ReturnedConnection, s.Error 233 | } 234 | 235 | type MockBasicRabbitDialer struct { 236 | Called bool 237 | ReturnedConnection *amqp.Connection 238 | Error error 239 | } 240 | 241 | func (s *MockBasicRabbitDialer) Dial(connectionURL string) (*amqp.Connection, error) { 242 | s.Called = true 243 | return s.ReturnedConnection, s.Error 244 | } 245 | 246 | type MockCertPoolMaker struct { 247 | Called bool 248 | AppendedCaCert []byte 249 | CertPoolToReturn *x509.CertPool 250 | } 251 | 252 | func (x *MockCertPoolMaker) NewCertPoolWithAppendedCa(caCert []byte) *x509.CertPool { 253 | x.AppendedCaCert = caCert 254 | x.Called = true 255 | return x.CertPoolToReturn 256 | } 257 | 258 | func createDummyAmqpConnection() *amqp.Connection { 259 | return &amqp.Connection{} 260 | } 261 | -------------------------------------------------------------------------------- /consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 4 | 5 | // Client intarface for consuming messages 6 | type Client interface { 7 | Name() string 8 | Start(forwarder.Client, chan bool, chan bool) error 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | rabbitamazonforwarder: 4 | image: airhelp/rabbit-amazon-forwarder 5 | ports: 6 | - "8080:8080" 7 | volumes: 8 | - "${MAPPING_FILE}:/config/mapping.json" 9 | - "${CERTS_DIR:-./certs}:/certs" 10 | environment: 11 | MAPPING_FILE: /config/mapping.json 12 | AWS_REGION: ${AWS_REGION} 13 | AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} 14 | AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} 15 | CA_CERT_FILE: ${CA_CERT_FILE:- } 16 | CERT_FILE: ${CERT_FILE:- } 17 | KEY_FILE: ${KEY_FILE:- } 18 | tests: 19 | build: 20 | context: . 21 | dockerfile: Dockerfile-test 22 | command: go test ./... 23 | depends_on: 24 | - fmt 25 | - vet 26 | vet: 27 | build: 28 | context: . 29 | dockerfile: Dockerfile-test 30 | command: go vet -v ./... 31 | fmt: 32 | build: 33 | context: . 34 | dockerfile: Dockerfile-test 35 | command: gofmt ./... 36 | -------------------------------------------------------------------------------- /examples/rabbit_to_lambda.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source" : { 4 | "type" : "RabbitMQ", 5 | "name" : "test-rabbit", 6 | "connection" : "amqp://guest:guest@localhost:5672/", 7 | "topic" : "amq.topic", 8 | "queue" : "test-queue", 9 | "routingKeys" : ["#"] 10 | }, 11 | "destination" : { 12 | "type" : "Lambda", 13 | "name" : "test-lambda", 14 | "target" : "test-forwarder-lambda" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /examples/rabbit_to_sns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source" : { 4 | "type" : "RabbitMQ", 5 | "name" : "test-rabbit", 6 | "connection" : "amqp://guest:guest@localhost:5672/", 7 | "topic" : "amq.topic", 8 | "queue" : "test-queue", 9 | "routingKeys" : ["#"] 10 | }, 11 | "destination" : { 12 | "type" : "SNS", 13 | "name" : "test-sns", 14 | "target" : "arn:aws:sns:eu-west-1:XXXXXXXX:test-forwarder" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /examples/rabbit_to_sqs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source" : { 4 | "type" : "RabbitMQ", 5 | "name" : "test-rabbit", 6 | "connection" : "amqp://guest:guest@localhost:5672/", 7 | "topic" : "amq.topic", 8 | "queue" : "test-queue", 9 | "routingKeys" : ["#"] 10 | }, 11 | "destination" : { 12 | "type" : "SQS", 13 | "name" : "test-queue", 14 | "target" : "https://sqs.eu-west-1.amazonaws.com/XXXXXXXXX/test-queue" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /forwarder/forwarder.go: -------------------------------------------------------------------------------- 1 | package forwarder 2 | 3 | const ( 4 | // EmptyMessageError empty error message 5 | EmptyMessageError = "message is empty" 6 | ) 7 | 8 | // Client interface to forwarding messages 9 | type Client interface { 10 | Name() string 11 | Push(message string) error 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AirHelp/rabbit-amazon-forwarder 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.43.32 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.19.0 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/streadway/amqp v1.0.0 11 | ) 12 | 13 | require ( 14 | github.com/fsnotify/fsnotify v1.4.9 // indirect 15 | github.com/jmespath/go-jmespath v0.4.0 // indirect 16 | github.com/nxadm/tail v1.4.8 // indirect 17 | golang.org/x/net v0.33.0 // indirect 18 | golang.org/x/sys v0.28.0 // indirect 19 | golang.org/x/text v0.21.0 // indirect 20 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 21 | gopkg.in/yaml.v2 v2.4.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.43.32 h1:b2NQnfWfImfo7yzXq6gzXEC+6s5v1t2RU3G9o+VirYo= 2 | github.com/aws/aws-sdk-go v1.43.32/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 7 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 10 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 12 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 13 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 14 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 15 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 16 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 17 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 18 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 19 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 20 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 22 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 23 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 24 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 25 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 26 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 27 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 28 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 31 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 32 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 33 | github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= 34 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 35 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 36 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 37 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 38 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 42 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 43 | github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= 44 | github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 47 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 48 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 49 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 52 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 53 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 54 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 57 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 58 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 59 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 60 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 61 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 62 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 78 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 79 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 81 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 82 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 83 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 84 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 87 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 88 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 93 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 94 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 95 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 96 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 97 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 98 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 102 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 103 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 104 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 105 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 106 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 107 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 108 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 109 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 110 | -------------------------------------------------------------------------------- /img/rabbit-amazon-forwarder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirHelp/rabbit-amazon-forwarder/79ab0ef27cf7f5d102b3783e8c62f3583af78f42/img/rabbit-amazon-forwarder.png -------------------------------------------------------------------------------- /lambda/forwarder.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "errors" 5 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 6 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/lambda" 10 | "github.com/aws/aws-sdk-go/service/lambda/lambdaiface" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | // Type forwarder type 16 | Type = "Lambda" 17 | ) 18 | 19 | // Forwarder forwarding client 20 | type Forwarder struct { 21 | name string 22 | lambdaClient lambdaiface.LambdaAPI 23 | function string 24 | } 25 | 26 | // CreateForwarder creates instance of forwarder 27 | func CreateForwarder(entry config.AmazonEntry, lambdaClient ...lambdaiface.LambdaAPI) forwarder.Client { 28 | var client lambdaiface.LambdaAPI 29 | if len(lambdaClient) > 0 { 30 | client = lambdaClient[0] 31 | } else { 32 | client = lambda.New(session.Must(session.NewSession())) 33 | } 34 | forwarder := Forwarder{entry.Name, client, entry.Target} 35 | log.WithField("forwarderName", forwarder.Name()).Info("Created forwarder") 36 | return forwarder 37 | } 38 | 39 | // Name forwarder name 40 | func (f Forwarder) Name() string { 41 | return f.name 42 | } 43 | 44 | // Push pushes message to forwarding infrastructure 45 | func (f Forwarder) Push(message string) error { 46 | if message == "" { 47 | return errors.New(forwarder.EmptyMessageError) 48 | } 49 | params := &lambda.InvokeInput{ 50 | FunctionName: aws.String(f.function), 51 | Payload: []byte(message), 52 | } 53 | resp, err := f.lambdaClient.Invoke(params) 54 | if err != nil { 55 | log.WithFields(log.Fields{ 56 | "forwarderName": f.Name(), 57 | "error": err.Error()}).Error("Could not forward message") 58 | return err 59 | } 60 | if resp.FunctionError != nil { 61 | log.WithFields(log.Fields{ 62 | "forwarderName": f.Name(), 63 | "functionError": *resp.FunctionError}).Errorf("Could not forward message") 64 | return errors.New(*resp.FunctionError) 65 | } 66 | log.WithFields(log.Fields{ 67 | "forwarderName": f.Name(), 68 | "statusCode": resp.StatusCode}).Info("Forward succeeded") 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /lambda/forwarder_test.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 8 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/lambda" 11 | "github.com/aws/aws-sdk-go/service/lambda/lambdaiface" 12 | ) 13 | 14 | const ( 15 | badRequest = "Bad request" 16 | handlerError = "Handled" 17 | unhandledError = "Unhandled" 18 | ) 19 | 20 | func TestCreateForwarder(t *testing.T) { 21 | entry := config.AmazonEntry{Type: "Lambda", 22 | Name: "lambda-test", 23 | Target: "function1-test", 24 | } 25 | forwarder := CreateForwarder(entry) 26 | if forwarder.Name() != entry.Name { 27 | t.Errorf("wrong forwarder name, expected:%s, found: %s", entry.Name, forwarder.Name()) 28 | } 29 | } 30 | 31 | func TestPush(t *testing.T) { 32 | functionName := "function1-test" 33 | entry := config.AmazonEntry{Type: "Lambda", 34 | Name: "lambda-test", 35 | Target: functionName, 36 | } 37 | scenarios := []struct { 38 | name string 39 | mock lambdaiface.LambdaAPI 40 | message string 41 | function string 42 | err error 43 | }{ 44 | { 45 | name: "empty message", 46 | mock: mockAmazonLambda{resp: lambda.InvokeOutput{StatusCode: aws.Int64(202)}, function: functionName, message: ""}, 47 | message: "", 48 | function: functionName, 49 | err: errors.New(forwarder.EmptyMessageError), 50 | }, 51 | { 52 | name: "bad request", 53 | mock: mockAmazonLambda{resp: lambda.InvokeOutput{StatusCode: aws.Int64(202)}, function: functionName, message: badRequest}, 54 | message: badRequest, 55 | function: functionName, 56 | err: errors.New(badRequest), 57 | }, 58 | { 59 | name: "handled error", 60 | mock: mockAmazonLambda{resp: lambda.InvokeOutput{StatusCode: aws.Int64(202), FunctionError: aws.String(handlerError)}, function: functionName, message: handlerError}, 61 | message: handlerError, 62 | function: functionName, 63 | err: errors.New(handlerError), 64 | }, 65 | { 66 | name: "unhandled error", 67 | mock: mockAmazonLambda{resp: lambda.InvokeOutput{StatusCode: aws.Int64(202), FunctionError: aws.String(unhandledError)}, function: functionName, message: unhandledError}, 68 | message: unhandledError, 69 | function: functionName, 70 | err: errors.New(unhandledError), 71 | }, 72 | { 73 | name: "success", 74 | mock: mockAmazonLambda{resp: lambda.InvokeOutput{StatusCode: aws.Int64(202)}, function: functionName, message: "abc"}, 75 | message: "abc", 76 | function: functionName, 77 | err: nil, 78 | }, 79 | } 80 | for _, scenario := range scenarios { 81 | t.Log("Scenario name: ", scenario.name) 82 | forwarder := CreateForwarder(entry, scenario.mock) 83 | err := forwarder.Push(scenario.message) 84 | if scenario.err == nil && err != nil { 85 | t.Errorf("Error should not occur. Error: %s", err.Error()) 86 | return 87 | } 88 | if scenario.err == err { 89 | return 90 | } 91 | if err == nil { 92 | t.Errorf("Error should occur. Expected: %s", scenario.err.Error()) 93 | return 94 | } 95 | if err.Error() != scenario.err.Error() { 96 | t.Errorf("Wrong error, expecting:%v, got:%v", scenario.err, err) 97 | } 98 | } 99 | } 100 | 101 | type mockAmazonLambda struct { 102 | lambdaiface.LambdaAPI 103 | resp lambda.InvokeOutput 104 | function string 105 | message string 106 | } 107 | 108 | func (m mockAmazonLambda) Invoke(input *lambda.InvokeInput) (*lambda.InvokeOutput, error) { 109 | if *input.FunctionName != m.function { 110 | return nil, errors.New("Wrong function name") 111 | } 112 | if string(input.Payload) != m.message { 113 | return nil, errors.New("Wrong message body") 114 | } 115 | if string(input.Payload) == badRequest { 116 | return nil, errors.New(badRequest) 117 | } 118 | return &m.resp, nil 119 | } 120 | -------------------------------------------------------------------------------- /mapping/mapping.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/AirHelp/rabbit-amazon-forwarder/connector" 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 12 | "github.com/AirHelp/rabbit-amazon-forwarder/consumer" 13 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 14 | "github.com/AirHelp/rabbit-amazon-forwarder/lambda" 15 | "github.com/AirHelp/rabbit-amazon-forwarder/rabbitmq" 16 | "github.com/AirHelp/rabbit-amazon-forwarder/sns" 17 | "github.com/AirHelp/rabbit-amazon-forwarder/sqs" 18 | ) 19 | 20 | type pairs []pair 21 | 22 | type pair struct { 23 | Source config.RabbitEntry `json:"source"` 24 | Destination config.AmazonEntry `json:"destination"` 25 | } 26 | 27 | // Client mapping client 28 | type Client struct { 29 | helper Helper 30 | } 31 | 32 | // Helper interface for creating consumers and forwaders 33 | type Helper interface { 34 | createConsumer(entry config.RabbitEntry) consumer.Client 35 | createForwarder(entry config.AmazonEntry) forwarder.Client 36 | } 37 | 38 | // ConsumerForwarderMapping mapping for consumers and forwarders 39 | type ConsumerForwarderMapping struct { 40 | Consumer consumer.Client 41 | Forwarder forwarder.Client 42 | } 43 | 44 | type helperImpl struct{} 45 | 46 | // New creates new mapping client 47 | func New(helpers ...Helper) Client { 48 | var helper Helper 49 | helper = helperImpl{} 50 | if len(helpers) > 0 { 51 | helper = helpers[0] 52 | } 53 | return Client{helper} 54 | } 55 | 56 | // Load loads mappings 57 | func (c Client) Load() ([]ConsumerForwarderMapping, error) { 58 | var consumerForwarderMapping []ConsumerForwarderMapping 59 | data, err := c.loadFile() 60 | if err != nil { 61 | return consumerForwarderMapping, err 62 | } 63 | var pairsList pairs 64 | if err = json.Unmarshal(data, &pairsList); err != nil { 65 | return consumerForwarderMapping, err 66 | } 67 | log.Info("Loading consumer - forwarder pairs") 68 | for _, pair := range pairsList { 69 | consumer := c.helper.createConsumer(pair.Source) 70 | forwarder := c.helper.createForwarder(pair.Destination) 71 | consumerForwarderMapping = append(consumerForwarderMapping, ConsumerForwarderMapping{consumer, forwarder}) 72 | } 73 | return consumerForwarderMapping, nil 74 | } 75 | 76 | func (c Client) loadFile() ([]byte, error) { 77 | filePath := os.Getenv(config.MappingFile) 78 | log.WithField("mappingFile", filePath).Info("Loading mapping file") 79 | return ioutil.ReadFile(filePath) 80 | } 81 | 82 | func (h helperImpl) createConsumer(entry config.RabbitEntry) consumer.Client { 83 | log.WithFields(log.Fields{ 84 | "consumerType": entry.Type, 85 | "consumerName": entry.Name}).Info("Creating consumer") 86 | switch entry.Type { 87 | case rabbitmq.Type: 88 | rabbitConnector := connector.CreateConnector(entry.ConnectionURL) 89 | return rabbitmq.CreateConsumer(entry, rabbitConnector) 90 | } 91 | return nil 92 | } 93 | 94 | func (h helperImpl) createForwarder(entry config.AmazonEntry) forwarder.Client { 95 | log.WithFields(log.Fields{ 96 | "forwarderType": entry.Type, 97 | "forwarderName": entry.Name}).Info("Creating forwarder") 98 | switch entry.Type { 99 | case sns.Type: 100 | return sns.CreateForwarder(entry) 101 | case sqs.Type: 102 | return sqs.CreateForwarder(entry) 103 | case lambda.Type: 104 | return lambda.CreateForwarder(entry) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /mapping/mapping_test.go: -------------------------------------------------------------------------------- 1 | package mapping 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 9 | "github.com/AirHelp/rabbit-amazon-forwarder/consumer" 10 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 11 | "github.com/AirHelp/rabbit-amazon-forwarder/lambda" 12 | "github.com/AirHelp/rabbit-amazon-forwarder/rabbitmq" 13 | "github.com/AirHelp/rabbit-amazon-forwarder/sns" 14 | "github.com/AirHelp/rabbit-amazon-forwarder/sqs" 15 | ) 16 | 17 | const ( 18 | rabbitType = "rabbit" 19 | snsType = "sns" 20 | ) 21 | 22 | func TestLoad(t *testing.T) { 23 | os.Setenv(config.MappingFile, "../tests/rabbit_to_sns.json") 24 | client := New(MockMappingHelper{}) 25 | var consumerForwarderMapping []ConsumerForwarderMapping 26 | var err error 27 | if consumerForwarderMapping, err = client.Load(); err != nil { 28 | t.Errorf("could not load mapping and start mocked rabbit->sns pair: %s", err.Error()) 29 | } 30 | if len(consumerForwarderMapping) != 1 { 31 | t.Errorf("wrong consumerForwarderMapping size, expected 1, got %d", len(consumerForwarderMapping)) 32 | } 33 | } 34 | 35 | func TestLoadFile(t *testing.T) { 36 | os.Setenv(config.MappingFile, "../tests/rabbit_to_sns.json") 37 | client := New() 38 | data, err := client.loadFile() 39 | if err != nil { 40 | t.Errorf("could not load file: %s", err.Error()) 41 | } 42 | if len(data) < 1 { 43 | t.Errorf("could not load file: empty steam found") 44 | } 45 | } 46 | 47 | func TestCreateConsumer(t *testing.T) { 48 | client := New() 49 | consumerName := "test-rabbit" 50 | entry := config.RabbitEntry{Type: "RabbitMQ", 51 | Name: consumerName, 52 | ConnectionURL: "url", 53 | ExchangeName: "topic", 54 | QueueName: "test-queue", 55 | RoutingKey: "#"} 56 | consumer := client.helper.createConsumer(entry) 57 | if consumer.Name() != consumerName { 58 | t.Errorf("wrong consumer name, expected %s, found %s", consumerName, consumer.Name()) 59 | } 60 | rabbitConsumer := consumer.(rabbitmq.Consumer) 61 | if rabbitConsumer.RabbitConnector == nil { 62 | t.Errorf("rabbit consumer should have been set") 63 | } 64 | } 65 | 66 | func TestCreateForwarderSNS(t *testing.T) { 67 | client := New(MockMappingHelper{}) 68 | forwarderName := "test-sns" 69 | entry := config.AmazonEntry{Type: "SNS", 70 | Name: forwarderName, 71 | Target: "arn", 72 | } 73 | forwarder := client.helper.createForwarder(entry) 74 | if forwarder.Name() != forwarderName { 75 | t.Errorf("wrong forwarder name, expected %s, found %s", forwarderName, forwarder.Name()) 76 | } 77 | } 78 | 79 | func TestCreateForwarderSQS(t *testing.T) { 80 | client := New(MockMappingHelper{}) 81 | forwarderName := "test-sqs" 82 | entry := config.AmazonEntry{Type: "SQS", 83 | Name: forwarderName, 84 | Target: "arn", 85 | } 86 | forwarder := client.helper.createForwarder(entry) 87 | if forwarder.Name() != forwarderName { 88 | t.Errorf("wrong forwarder name, expected %s, found %s", forwarderName, forwarder.Name()) 89 | } 90 | } 91 | 92 | func TestCreateForwarderLambda(t *testing.T) { 93 | client := New(MockMappingHelper{}) 94 | forwarderName := "test-lambda" 95 | entry := config.AmazonEntry{Type: "Lambda", 96 | Name: forwarderName, 97 | Target: "function-name", 98 | } 99 | forwarder := client.helper.createForwarder(entry) 100 | if forwarder.Name() != forwarderName { 101 | t.Errorf("wrong forwarder name, expected %s, found %s", forwarderName, forwarder.Name()) 102 | } 103 | } 104 | 105 | // helpers 106 | type MockMappingHelper struct{} 107 | 108 | type MockRabbitConsumer struct{} 109 | 110 | type MockSNSForwarder struct { 111 | name string 112 | } 113 | 114 | type MockSQSForwarder struct { 115 | name string 116 | } 117 | 118 | type MockLambdaForwarder struct { 119 | name string 120 | } 121 | 122 | type ErrorForwarder struct{} 123 | 124 | func (h MockMappingHelper) createConsumer(entry config.RabbitEntry) consumer.Client { 125 | if entry.Type != rabbitmq.Type { 126 | return nil 127 | } 128 | return MockRabbitConsumer{} 129 | } 130 | func (h MockMappingHelper) createForwarder(entry config.AmazonEntry) forwarder.Client { 131 | switch entry.Type { 132 | case sns.Type: 133 | return MockSNSForwarder{entry.Name} 134 | case sqs.Type: 135 | return MockSQSForwarder{entry.Name} 136 | case lambda.Type: 137 | return MockLambdaForwarder{entry.Name} 138 | } 139 | return ErrorForwarder{} 140 | } 141 | 142 | func (c MockRabbitConsumer) Name() string { 143 | return rabbitType 144 | } 145 | 146 | func (c MockRabbitConsumer) Start(client forwarder.Client, check chan bool, stop chan bool) error { 147 | return nil 148 | } 149 | 150 | func (f MockSNSForwarder) Name() string { 151 | return f.name 152 | } 153 | 154 | func (f MockSNSForwarder) Push(message string) error { 155 | return nil 156 | } 157 | 158 | func (f MockSQSForwarder) Name() string { 159 | return f.name 160 | } 161 | 162 | func (f MockLambdaForwarder) Push(message string) error { 163 | return nil 164 | } 165 | 166 | func (f MockLambdaForwarder) Name() string { 167 | return f.name 168 | } 169 | 170 | func (f MockSQSForwarder) Push(message string) error { 171 | return nil 172 | } 173 | 174 | func (f ErrorForwarder) Name() string { 175 | return "error-forwarder" 176 | } 177 | 178 | func (f ErrorForwarder) Push(message string) error { 179 | return errors.New("Wrong forwader created") 180 | } 181 | -------------------------------------------------------------------------------- /rabbitmq/consumer.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 11 | "github.com/AirHelp/rabbit-amazon-forwarder/connector" 12 | "github.com/AirHelp/rabbit-amazon-forwarder/consumer" 13 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 14 | "github.com/streadway/amqp" 15 | ) 16 | 17 | const ( 18 | // Type consumer type 19 | Type = "RabbitMQ" 20 | channelClosedMessage = "Channel closed" 21 | closedBySupervisorMessage = "Closed by supervisor" 22 | // ReconnectRabbitMQInterval time to reconnect 23 | ReconnectRabbitMQInterval = 10 24 | ) 25 | 26 | // Consumer implementation or RabbitMQ consumer 27 | type Consumer struct { 28 | name string 29 | ConnectionURL string 30 | ExchangeName string 31 | QueueName string 32 | RoutingKeys []string 33 | RabbitConnector connector.RabbitConnector 34 | } 35 | 36 | // parameters for starting consumer 37 | type workerParams struct { 38 | forwarder forwarder.Client 39 | msgs <-chan amqp.Delivery 40 | check chan bool 41 | stop chan bool 42 | conn *amqp.Connection 43 | ch *amqp.Channel 44 | } 45 | 46 | // CreateConsumer creates consumer from string map 47 | func CreateConsumer(entry config.RabbitEntry, rabbitConnector connector.RabbitConnector) consumer.Client { 48 | // merge RoutingKey with RoutingKeys 49 | if entry.RoutingKey != "" { 50 | entry.RoutingKeys = append(entry.RoutingKeys, entry.RoutingKey) 51 | } 52 | return Consumer{entry.Name, entry.ConnectionURL, entry.ExchangeName, entry.QueueName, entry.RoutingKeys, rabbitConnector} 53 | } 54 | 55 | // Name consumer name 56 | func (c Consumer) Name() string { 57 | return c.name 58 | } 59 | 60 | // Start start consuming messages from Rabbit queue 61 | func (c Consumer) Start(forwarder forwarder.Client, check chan bool, stop chan bool) error { 62 | log.WithFields(log.Fields{ 63 | "exchangeName": c.ExchangeName, 64 | "queueName": c.QueueName}).Info("Starting connecting consumer") 65 | for { 66 | delivery, conn, ch, err := c.initRabbitMQ() 67 | if err != nil { 68 | log.Error(err) 69 | closeRabbitMQ(conn, ch) 70 | time.Sleep(ReconnectRabbitMQInterval * time.Second) 71 | continue 72 | } 73 | params := workerParams{forwarder, delivery, check, stop, conn, ch} 74 | if err := c.startForwarding(¶ms); err.Error() == closedBySupervisorMessage { 75 | break 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | func closeRabbitMQ(conn *amqp.Connection, ch *amqp.Channel) { 82 | log.Info("Closing RabbitMQ connection and channel") 83 | if ch != nil { 84 | if err := ch.Close(); err != nil { 85 | log.WithField("error", err.Error()).Error("Could not close channel") 86 | } 87 | } 88 | if conn != nil { 89 | if err := conn.Close(); err != nil { 90 | log.WithField("error", err.Error()).Error("Could not close connection") 91 | } 92 | } 93 | } 94 | 95 | func (c Consumer) initRabbitMQ() (<-chan amqp.Delivery, *amqp.Connection, *amqp.Channel, error) { 96 | _, connection, channel, err := c.connect() 97 | if err != nil { 98 | return nil, connection, channel, err 99 | } 100 | delivery, _, _, err := c.setupExchangesAndQueues(connection, channel) 101 | return delivery, connection, channel, err 102 | } 103 | 104 | func (c Consumer) connect() (<-chan amqp.Delivery, *amqp.Connection, *amqp.Channel, error) { 105 | conn, err := c.RabbitConnector.CreateConnection(c.ConnectionURL) 106 | if err != nil { 107 | return failOnError(err, "Failed to connect to RabbitMQ") 108 | } 109 | ch, err := conn.Channel() 110 | if err != nil { 111 | return failOnError(err, "Failed to open a channel") 112 | } 113 | return nil, conn, ch, nil 114 | } 115 | 116 | func (c Consumer) setupExchangesAndQueues(conn *amqp.Connection, ch *amqp.Channel) (<-chan amqp.Delivery, *amqp.Connection, *amqp.Channel, error) { 117 | var err error 118 | deadLetterExchangeName := c.QueueName + "-dead-letter" 119 | deadLetterQueueName := c.QueueName + "-dead-letter" 120 | // regular exchange 121 | if err = ch.ExchangeDeclare(c.ExchangeName, "topic", true, false, false, false, nil); err != nil { 122 | return failOnError(err, "Failed to declare an exchange:"+c.ExchangeName) 123 | } 124 | // dead-letter-exchange 125 | if err = ch.ExchangeDeclare(deadLetterExchangeName, "fanout", true, false, false, false, nil); err != nil { 126 | return failOnError(err, "Failed to declare an exchange:"+deadLetterExchangeName) 127 | } 128 | // dead-letter-queue 129 | if _, err = ch.QueueDeclare(deadLetterQueueName, true, false, false, false, nil); err != nil { 130 | return failOnError(err, "Failed to declare a queue:"+deadLetterQueueName) 131 | } 132 | if err = ch.QueueBind(deadLetterQueueName, "#", deadLetterExchangeName, false, nil); err != nil { 133 | return failOnError(err, "Failed to bind a queue:"+deadLetterQueueName) 134 | } 135 | // regular queue 136 | if _, err = ch.QueueDeclare(c.QueueName, true, false, false, false, 137 | amqp.Table{ 138 | "x-dead-letter-exchange": deadLetterExchangeName, 139 | }); err != nil { 140 | return failOnError(err, "Failed to declare a queue:"+c.QueueName) 141 | } 142 | // bind all of the routing keys 143 | for _, routingKey := range c.RoutingKeys { 144 | if err = ch.QueueBind(c.QueueName, routingKey, c.ExchangeName, false, nil); err != nil { 145 | return failOnError(err, "Failed to bind a queue:"+c.QueueName) 146 | } 147 | } 148 | 149 | msgs, err := ch.Consume(c.QueueName, c.Name(), false, false, false, false, nil) 150 | if err != nil { 151 | return failOnError(err, "Failed to register a consumer") 152 | } 153 | return msgs, nil, nil, nil 154 | } 155 | 156 | func (c Consumer) startForwarding(params *workerParams) error { 157 | forwarderName := params.forwarder.Name() 158 | log.WithFields(log.Fields{ 159 | "consumerName": c.Name(), 160 | "forwarderName": forwarderName}).Info("Started forwarding messages") 161 | for { 162 | select { 163 | case d, ok := <-params.msgs: 164 | if !ok { // channel already closed 165 | closeRabbitMQ(params.conn, params.ch) 166 | return errors.New(channelClosedMessage) 167 | } 168 | log.WithFields(log.Fields{ 169 | "consumerName": c.Name(), 170 | "messageID": d.MessageId}).Info("Message to forward") 171 | err := params.forwarder.Push(string(d.Body)) 172 | if err != nil { 173 | log.WithFields(log.Fields{ 174 | "forwarderName": forwarderName, 175 | "error": err.Error()}).Error("Could not forward message") 176 | if err = d.Reject(false); err != nil { 177 | log.WithFields(log.Fields{ 178 | "forwarderName": forwarderName, 179 | "error": err.Error()}).Error("Could not reject message") 180 | return err 181 | } 182 | 183 | } else { 184 | if err := d.Ack(true); err != nil { 185 | log.WithFields(log.Fields{ 186 | "forwarderName": forwarderName, 187 | "error": err.Error(), 188 | "messageID": d.MessageId}).Error("Could not ack message") 189 | return err 190 | } 191 | } 192 | case <-params.check: 193 | log.WithField("forwarderName", forwarderName).Info("Checking") 194 | case <-params.stop: 195 | log.WithField("forwarderName", forwarderName).Info("Closing") 196 | closeRabbitMQ(params.conn, params.ch) 197 | return errors.New(closedBySupervisorMessage) 198 | } 199 | } 200 | } 201 | 202 | func failOnError(err error, msg string) (<-chan amqp.Delivery, *amqp.Connection, *amqp.Channel, error) { 203 | return nil, nil, nil, fmt.Errorf("%s: %s", msg, err) 204 | } 205 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/AirHelp/rabbit-amazon-forwarder/mapping" 5 | "github.com/AirHelp/rabbit-amazon-forwarder/supervisor" 6 | log "github.com/sirupsen/logrus" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | const ( 12 | LogLevel = "LOG_LEVEL" 13 | ) 14 | 15 | func main() { 16 | createLogger() 17 | 18 | consumerForwarderMapping, err := mapping.New().Load() 19 | if err != nil { 20 | log.WithField("error", err.Error()).Fatalf("Could not load consumer - forwarder pairs") 21 | } 22 | supervisor := supervisor.New(consumerForwarderMapping) 23 | if err := supervisor.Start(); err != nil { 24 | log.WithField("error", err.Error()).Fatal("Could not start supervisor") 25 | } 26 | http.HandleFunc("/restart", supervisor.Restart) 27 | http.HandleFunc("/health", supervisor.Check) 28 | log.Info("Starting http server") 29 | log.Fatal(http.ListenAndServe(":8080", nil)) 30 | } 31 | 32 | func createLogger() { 33 | log.SetFormatter(&log.JSONFormatter{}) 34 | log.SetOutput(os.Stdout) 35 | log.SetLevel(log.InfoLevel) 36 | if logLevel := os.Getenv(LogLevel); logLevel != "" { 37 | if level, err := log.ParseLevel(logLevel); err != nil { 38 | log.Fatal(err) 39 | } else { 40 | log.SetLevel(level) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sns/forwarder.go: -------------------------------------------------------------------------------- 1 | package sns 2 | 3 | import ( 4 | "errors" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 8 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/sns" 12 | "github.com/aws/aws-sdk-go/service/sns/snsiface" 13 | ) 14 | 15 | const ( 16 | // Type forwarder type 17 | Type = "SNS" 18 | ) 19 | 20 | // Forwarder forwarding client 21 | type Forwarder struct { 22 | name string 23 | snsClient snsiface.SNSAPI 24 | topic string 25 | } 26 | 27 | // CreateForwarder creates instance of forwarder 28 | func CreateForwarder(entry config.AmazonEntry, snsClient ...snsiface.SNSAPI) forwarder.Client { 29 | var client snsiface.SNSAPI 30 | if len(snsClient) > 0 { 31 | client = snsClient[0] 32 | } else { 33 | client = sns.New(session.Must(session.NewSession())) 34 | } 35 | forwarder := Forwarder{entry.Name, client, entry.Target} 36 | log.WithField("forwarderName", forwarder.Name()).Info("Created forwarder") 37 | return forwarder 38 | } 39 | 40 | // Name forwarder name 41 | func (f Forwarder) Name() string { 42 | return f.name 43 | } 44 | 45 | // Push pushes message to forwarding infrastructure 46 | func (f Forwarder) Push(message string) error { 47 | if message == "" { 48 | return errors.New(forwarder.EmptyMessageError) 49 | } 50 | params := &sns.PublishInput{ 51 | Message: aws.String(message), 52 | TargetArn: aws.String(f.topic), 53 | } 54 | 55 | resp, err := f.snsClient.Publish(params) 56 | if err != nil { 57 | log.WithFields(log.Fields{ 58 | "forwarderName": f.Name(), 59 | "error": err.Error()}).Error("Could not forward message") 60 | return err 61 | } 62 | log.WithFields(log.Fields{ 63 | "forwarderName": f.Name(), 64 | "responseID": resp.MessageId}).Info("Forward succeeded") 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /sns/forwarder_test.go: -------------------------------------------------------------------------------- 1 | package sns 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 8 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/sns" 11 | "github.com/aws/aws-sdk-go/service/sns/snsiface" 12 | ) 13 | 14 | var badRequest = "Bad request" 15 | 16 | func TestCreateForwarder(t *testing.T) { 17 | entry := config.AmazonEntry{Type: "SNS", 18 | Name: "sns-test", 19 | Target: "arn", 20 | } 21 | forwarder := CreateForwarder(entry) 22 | if forwarder.Name() != entry.Name { 23 | t.Errorf("wrong forwarder name, expected:%s, found: %s", entry.Name, forwarder.Name()) 24 | } 25 | } 26 | 27 | func TestPush(t *testing.T) { 28 | topicName := "topic1" 29 | entry := config.AmazonEntry{Type: "SNS", 30 | Name: "sns-test", 31 | Target: topicName, 32 | } 33 | scenarios := []struct { 34 | name string 35 | mock snsiface.SNSAPI 36 | message string 37 | topic string 38 | err error 39 | }{ 40 | { 41 | name: "empty message", 42 | mock: mockAmazonSNS{resp: sns.PublishOutput{MessageId: aws.String("messageId")}, topic: topicName, message: ""}, 43 | message: "", 44 | topic: topicName, 45 | err: errors.New(forwarder.EmptyMessageError), 46 | }, 47 | { 48 | name: "bad request", 49 | mock: mockAmazonSNS{resp: sns.PublishOutput{MessageId: aws.String("messageId")}, topic: topicName, message: badRequest}, 50 | message: badRequest, 51 | topic: topicName, 52 | err: errors.New(badRequest), 53 | }, 54 | { 55 | name: "success", 56 | mock: mockAmazonSNS{resp: sns.PublishOutput{MessageId: aws.String("messageId")}, topic: topicName, message: "abc"}, 57 | message: "abc", 58 | topic: topicName, 59 | err: nil, 60 | }, 61 | } 62 | for _, scenario := range scenarios { 63 | t.Log("Scenario name: ", scenario.name) 64 | forwarder := CreateForwarder(entry, scenario.mock) 65 | err := forwarder.Push(scenario.message) 66 | if scenario.err == nil && err != nil { 67 | t.Errorf("Error should not occur") 68 | return 69 | } 70 | if scenario.err == err { 71 | return 72 | } 73 | if err.Error() != scenario.err.Error() { 74 | t.Errorf("Wrong error, expecting:%v, got:%v", scenario.err, err) 75 | } 76 | } 77 | } 78 | 79 | type mockAmazonSNS struct { 80 | snsiface.SNSAPI 81 | resp sns.PublishOutput 82 | topic string 83 | message string 84 | } 85 | 86 | func (m mockAmazonSNS) Publish(input *sns.PublishInput) (*sns.PublishOutput, error) { 87 | if *input.TargetArn != m.topic { 88 | return nil, errors.New("Wrong topic name") 89 | } 90 | if *input.Message != m.message { 91 | return nil, errors.New("Wrong message body") 92 | } 93 | if *input.Message == badRequest { 94 | return nil, errors.New(badRequest) 95 | } 96 | return &m.resp, nil 97 | } 98 | -------------------------------------------------------------------------------- /sqs/forwarder.go: -------------------------------------------------------------------------------- 1 | package sqs 2 | 3 | import ( 4 | "errors" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 8 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/sqs" 12 | "github.com/aws/aws-sdk-go/service/sqs/sqsiface" 13 | ) 14 | 15 | const ( 16 | // Type forwarder type 17 | Type = "SQS" 18 | ) 19 | 20 | // Forwarder forwarding client 21 | type Forwarder struct { 22 | name string 23 | sqsClient sqsiface.SQSAPI 24 | queue string 25 | } 26 | 27 | // CreateForwarder creates instance of forwarder 28 | func CreateForwarder(entry config.AmazonEntry, sqsClient ...sqsiface.SQSAPI) forwarder.Client { 29 | var client sqsiface.SQSAPI 30 | if len(sqsClient) > 0 { 31 | client = sqsClient[0] 32 | } else { 33 | client = sqs.New(session.Must(session.NewSession())) 34 | } 35 | forwarder := Forwarder{entry.Name, client, entry.Target} 36 | log.WithField("forwarderName", forwarder.Name()).Info("Created forwarder") 37 | return forwarder 38 | } 39 | 40 | // Name forwarder name 41 | func (f Forwarder) Name() string { 42 | return f.name 43 | } 44 | 45 | // Push pushes message to forwarding infrastructure 46 | func (f Forwarder) Push(message string) error { 47 | if message == "" { 48 | return errors.New(forwarder.EmptyMessageError) 49 | } 50 | params := &sqs.SendMessageInput{ 51 | MessageBody: aws.String(message), // Required 52 | QueueUrl: aws.String(f.queue), // Required 53 | } 54 | 55 | resp, err := f.sqsClient.SendMessage(params) 56 | 57 | if err != nil { 58 | log.WithFields(log.Fields{ 59 | "forwarderName": f.Name(), 60 | "error": err.Error()}).Error("Could not forward message") 61 | return err 62 | } 63 | log.WithFields(log.Fields{ 64 | "forwarderName": f.Name(), 65 | "responseID": resp.MessageId}).Info("Forward succeeded") 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /sqs/forwarder_test.go: -------------------------------------------------------------------------------- 1 | package sqs 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/AirHelp/rabbit-amazon-forwarder/config" 8 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/sqs" 11 | "github.com/aws/aws-sdk-go/service/sqs/sqsiface" 12 | ) 13 | 14 | var badRequest = "Bad request" 15 | 16 | func TestCreateForwarder(t *testing.T) { 17 | entry := config.AmazonEntry{Type: "SQS", 18 | Name: "sqs-test", 19 | Target: "arn", 20 | } 21 | forwarder := CreateForwarder(entry) 22 | if forwarder.Name() != entry.Name { 23 | t.Errorf("wrong forwarder name, expected:%s, found: %s", entry.Name, forwarder.Name()) 24 | } 25 | } 26 | 27 | func TestPush(t *testing.T) { 28 | queueName := "queue1" 29 | entry := config.AmazonEntry{Type: "SQS", 30 | Name: "sqs-test", 31 | Target: queueName, 32 | } 33 | scenarios := []struct { 34 | name string 35 | mock sqsiface.SQSAPI 36 | message string 37 | queue string 38 | err error 39 | }{ 40 | { 41 | name: "empty message", 42 | mock: mockAmazonSQS{resp: sqs.SendMessageOutput{MessageId: aws.String("messageId")}, queue: queueName, message: ""}, 43 | message: "", 44 | queue: queueName, 45 | err: errors.New(forwarder.EmptyMessageError), 46 | }, 47 | { 48 | name: "bad request", 49 | mock: mockAmazonSQS{resp: sqs.SendMessageOutput{MessageId: aws.String("messageId")}, queue: queueName, message: badRequest}, 50 | message: badRequest, 51 | queue: queueName, 52 | err: errors.New(badRequest), 53 | }, 54 | { 55 | name: "success", 56 | mock: mockAmazonSQS{resp: sqs.SendMessageOutput{MessageId: aws.String("messageId")}, queue: queueName, message: "abc"}, 57 | message: "abc", 58 | queue: queueName, 59 | err: nil, 60 | }, 61 | } 62 | for _, scenario := range scenarios { 63 | t.Log("Scenario name: ", scenario.name) 64 | forwarder := CreateForwarder(entry, scenario.mock) 65 | err := forwarder.Push(scenario.message) 66 | if scenario.err == nil && err != nil { 67 | t.Errorf("Error should not occur") 68 | return 69 | } 70 | if scenario.err == err { 71 | return 72 | } 73 | if err.Error() != scenario.err.Error() { 74 | t.Errorf("Wrong error, expecting:%v, got:%v", scenario.err, err) 75 | } 76 | } 77 | } 78 | 79 | type mockAmazonSQS struct { 80 | sqsiface.SQSAPI 81 | resp sqs.SendMessageOutput 82 | queue string 83 | message string 84 | } 85 | 86 | func (m mockAmazonSQS) SendMessage(input *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) { 87 | if *input.QueueUrl != m.queue { 88 | return nil, errors.New("Wrong queue name") 89 | } 90 | if *input.MessageBody != m.message { 91 | return nil, errors.New("Wrong message body") 92 | } 93 | if *input.MessageBody == badRequest { 94 | return nil, errors.New(badRequest) 95 | } 96 | return &m.resp, nil 97 | } 98 | -------------------------------------------------------------------------------- /supervisor/supervisor.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/AirHelp/rabbit-amazon-forwarder/mapping" 13 | ) 14 | 15 | const ( 16 | jsonType = "application/json" 17 | success = "success" 18 | notSupported = "not supported response format" 19 | acceptHeader = "Accept" 20 | contentType = "Content-Type" 21 | acceptAll = "*/*" 22 | ) 23 | 24 | type response struct { 25 | Healthy bool `json:"healthy"` 26 | Message string `json:"message"` 27 | } 28 | 29 | type consumerChannel struct { 30 | name string 31 | check chan bool 32 | stop chan bool 33 | } 34 | 35 | // Client supervisor client 36 | type Client struct { 37 | mappings []mapping.ConsumerForwarderMapping 38 | consumers map[string]*consumerChannel 39 | } 40 | 41 | // New client for supervisor 42 | func New(consumerForwarderMapping []mapping.ConsumerForwarderMapping) Client { 43 | return Client{mappings: consumerForwarderMapping} 44 | } 45 | 46 | // Start starts supervisor 47 | func (c *Client) Start() error { 48 | c.consumers = make(map[string]*consumerChannel) 49 | for _, mappingEntry := range c.mappings { 50 | channel := makeConsumerChannel(mappingEntry.Forwarder.Name()) 51 | c.consumers[mappingEntry.Forwarder.Name()] = channel 52 | go mappingEntry.Consumer.Start(mappingEntry.Forwarder, channel.check, channel.stop) 53 | log.WithFields(log.Fields{ 54 | "consumerName": mappingEntry.Consumer.Name(), 55 | "forwarderName": mappingEntry.Forwarder.Name()}).Info("Started consumer with forwarder") 56 | } 57 | return nil 58 | } 59 | 60 | // Check checks running consumers 61 | func (c *Client) Check(w http.ResponseWriter, r *http.Request) { 62 | if accept := r.Header.Get(acceptHeader); accept != "" && 63 | !strings.Contains(accept, jsonType) && 64 | !strings.Contains(accept, acceptAll) { 65 | log.WithField("acceptHeader", accept).Warn("Wrong Accept header") 66 | notAcceptableResponse(w) 67 | return 68 | } 69 | stopped := 0 70 | for _, consumer := range c.consumers { 71 | if len(consumer.check) > 0 { 72 | stopped = stopped + 1 73 | continue 74 | } 75 | consumer.check <- true 76 | time.Sleep(500 * time.Millisecond) 77 | if len(consumer.check) > 0 { 78 | stopped = stopped + 1 79 | } 80 | } 81 | if stopped > 0 { 82 | message := fmt.Sprintf("Number of failed consumers: %d", stopped) 83 | errorResponse(w, message) 84 | return 85 | } 86 | successResponse(w) 87 | } 88 | 89 | // Restart restarts every consumer 90 | func (c *Client) Restart(w http.ResponseWriter, r *http.Request) { 91 | c.stop() 92 | if err := c.Start(); err != nil { 93 | log.Error(err) 94 | errorResponse(w, "") 95 | return 96 | } 97 | successResponse(w) 98 | } 99 | 100 | func (c *Client) stop() { 101 | for _, consumer := range c.consumers { 102 | consumer.stop <- true 103 | } 104 | } 105 | 106 | func makeConsumerChannel(name string) *consumerChannel { 107 | check := make(chan bool) 108 | stop := make(chan bool) 109 | return &consumerChannel{name: name, check: check, stop: stop} 110 | } 111 | 112 | func errorResponse(w http.ResponseWriter, message string) { 113 | w.Header().Set(contentType, jsonType) 114 | w.WriteHeader(500) 115 | w.Write([]byte(message)) 116 | } 117 | 118 | func notAcceptableResponse(w http.ResponseWriter) { 119 | w.Header().Set(contentType, jsonType) 120 | w.WriteHeader(406) 121 | bytes, err := json.Marshal(response{Healthy: false, Message: notSupported}) 122 | if err != nil { 123 | log.Error(err) 124 | w.WriteHeader(500) 125 | return 126 | } 127 | w.Write(bytes) 128 | } 129 | 130 | func successResponse(w http.ResponseWriter) { 131 | w.Header().Set(contentType, jsonType) 132 | w.WriteHeader(200) 133 | bytes, err := json.Marshal(response{Healthy: true, Message: success}) 134 | if err != nil { 135 | log.Error(err) 136 | w.WriteHeader(200) 137 | return 138 | } 139 | w.Write(bytes) 140 | } 141 | -------------------------------------------------------------------------------- /supervisor/supervisor_test.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/AirHelp/rabbit-amazon-forwarder/forwarder" 11 | "github.com/AirHelp/rabbit-amazon-forwarder/mapping" 12 | ) 13 | 14 | func TestStart(t *testing.T) { 15 | supervisor := New(prepareConsumers()) 16 | if err := supervisor.Start(); err != nil { 17 | t.Error("could not start supervised consumer->forwader pairs, error: ", err.Error()) 18 | } 19 | if len(supervisor.consumers) != 3 { 20 | t.Errorf("wrong number of consumer-forwarder pairs, expected:%d, got:%d: ", 3, len(supervisor.consumers)) 21 | } 22 | } 23 | 24 | func TestRestart(t *testing.T) { 25 | successJSON := response{Healthy: true, Message: success} 26 | sucessMessage, err := json.Marshal(successJSON) 27 | if err != nil { 28 | t.Error("Could not prepare response. Error: ", err.Error()) 29 | } 30 | supervisor := New(prepareConsumers()) 31 | req, err := http.NewRequest("GET", "/restart", nil) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | rr := httptest.NewRecorder() 36 | handler := http.HandlerFunc(supervisor.Restart) 37 | 38 | handler.ServeHTTP(rr, req) 39 | 40 | if rr.Code != 200 { 41 | t.Errorf("wrong status code, expected:%d, got:%d", rr.Code, 200) 42 | } 43 | if rr.Body.String() != string(sucessMessage) { 44 | t.Errorf("wrong response body, expected:%s, got:%v", "success", rr.Body.String()) 45 | } 46 | if rr.Header().Get(contentType) != jsonType { 47 | t.Errorf("wrong response header, expected:%s, got:%s", jsonType, rr.Header().Get(contentType)) 48 | } 49 | } 50 | 51 | func TestCheck(t *testing.T) { 52 | successJSON := response{Healthy: true, Message: success} 53 | sucessMessage, err := json.Marshal(successJSON) 54 | if err != nil { 55 | t.Error("Could not prepare response. Error: ", err.Error()) 56 | } 57 | notAccpetedJSON := response{Healthy: false, Message: notSupported} 58 | notAcceptedMessage, err := json.Marshal(notAccpetedJSON) 59 | if err != nil { 60 | t.Error("Could not prepare response. Error: ", err.Error()) 61 | } 62 | supervisor := New(prepareConsumers()) 63 | if err = supervisor.Start(); err != nil { 64 | t.Error("could not start supervised consumer->forwader pairs, error: ", err.Error()) 65 | } 66 | 67 | cases := []struct { 68 | httpCode int 69 | res string 70 | accept string 71 | }{ 72 | {200, string(sucessMessage), ""}, 73 | {200, string(sucessMessage), jsonType}, 74 | {200, string(sucessMessage), acceptAll}, 75 | {406, string(notAcceptedMessage), "plain/text"}, 76 | } 77 | for _, c := range cases { 78 | req, err := http.NewRequest("GET", "/check", nil) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | req.Header.Set("Accept", c.accept) 83 | rr := httptest.NewRecorder() 84 | handler := http.HandlerFunc(supervisor.Check) 85 | 86 | handler.ServeHTTP(rr, req) 87 | 88 | if rr.Code != c.httpCode { 89 | t.Errorf("wrong status code, expected:%d, got:%d", rr.Code, c.httpCode) 90 | } 91 | if rr.Body.String() != c.res { 92 | t.Errorf("wrong response body, expected:%s, got:%s", c.res, rr.Body.String()) 93 | } 94 | if rr.Header().Get(contentType) != jsonType { 95 | t.Errorf("wrong response header, expected:%s, got:%s", jsonType, rr.Header().Get(contentType)) 96 | } 97 | } 98 | } 99 | 100 | func prepareConsumers() []mapping.ConsumerForwarderMapping { 101 | var consumers []mapping.ConsumerForwarderMapping 102 | consumers = append(consumers, mapping.ConsumerForwarderMapping{Consumer: MockRabbitConsumer{"rabbit"}, Forwarder: MockSNSForwarder{"sns"}}) 103 | consumers = append(consumers, mapping.ConsumerForwarderMapping{Consumer: MockRabbitConsumer{"rabbit"}, Forwarder: MockSQSForwarder{"sqs"}}) 104 | consumers = append(consumers, mapping.ConsumerForwarderMapping{Consumer: MockRabbitConsumer{"rabbit"}, Forwarder: MockLambdaForwarder{"lambda"}}) 105 | return consumers 106 | } 107 | 108 | type MockRabbitConsumer struct { 109 | name string 110 | } 111 | 112 | type MockSNSForwarder struct { 113 | name string 114 | } 115 | 116 | type MockSQSForwarder struct { 117 | name string 118 | } 119 | 120 | type MockLambdaForwarder struct { 121 | name string 122 | } 123 | 124 | func (c MockRabbitConsumer) Name() string { 125 | return c.name 126 | } 127 | 128 | func (c MockRabbitConsumer) Start(client forwarder.Client, check chan bool, stop chan bool) error { 129 | go func() { 130 | for { 131 | select { 132 | case <-check: 133 | fmt.Print("Checked") 134 | } 135 | } 136 | }() 137 | return nil 138 | } 139 | 140 | func (f MockSNSForwarder) Name() string { 141 | return f.name 142 | } 143 | 144 | func (f MockSNSForwarder) Push(message string) error { 145 | return nil 146 | } 147 | 148 | func (f MockSQSForwarder) Name() string { 149 | return f.name 150 | } 151 | 152 | func (f MockSQSForwarder) Push(message string) error { 153 | return nil 154 | } 155 | 156 | func (f MockLambdaForwarder) Name() string { 157 | return f.name 158 | } 159 | 160 | func (f MockLambdaForwarder) Push(message string) error { 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /tests/rabbit_to_sns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source" : { 4 | "type" : "RabbitMQ", 5 | "name" : "test-rabbit", 6 | "connection" : "amqp://guest:guest@localhost:5672/", 7 | "topic" : "amq.topic", 8 | "queue" : "test-queue", 9 | "routing" : "#" 10 | }, 11 | "destination" : { 12 | "type" : "SNS", 13 | "name" : "test-sns", 14 | "target" : "arn:aws:sns:eu-west-1:XXXXXXXX:test-forwarder" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /tests/rabbit_to_sqs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source" : { 4 | "type" : "RabbitMQ", 5 | "name" : "test-rabbit", 6 | "connection" : "amqp://guest:guest@localhost:5672/", 7 | "topic" : "amq.topic", 8 | "queue" : "test-queue", 9 | "routing" : "#" 10 | }, 11 | "destination" : { 12 | "type" : "SQS", 13 | "name" : "test-queue", 14 | "target" : "https://sqs.eu-west-1.amazonaws.com/XXXXXXXXX/test-queue" 15 | } 16 | } 17 | ] 18 | --------------------------------------------------------------------------------